001: /**********************************************************************
002: Copyright (c) 2006 Erik Bengtson and others. All rights reserved.
003: Licensed under the Apache License, Version 2.0 (the "License");
004: you may not use this file except in compliance with the License.
005: You may obtain a copy of the License at
006:
007: http://www.apache.org/licenses/LICENSE-2.0
008:
009: Unless required by applicable law or agreed to in writing, software
010: distributed under the License is distributed on an "AS IS" BASIS,
011: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: See the License for the specific language governing permissions and
013: limitations under the License.
014:
015:
016: Contributors:
017: 2006 Thomas Marti - Added support for configurable plugin file names
018: 2007 André Fügenschuh - Support for protocol "jar:http:"
019: ...
020: **********************************************************************/package org.jpox.plugin;
021:
022: import java.io.File;
023: import java.io.FilenameFilter;
024: import java.io.IOException;
025: import java.io.InputStream;
026: import java.lang.reflect.Constructor;
027: import java.lang.reflect.InvocationTargetException;
028: import java.net.JarURLConnection;
029: import java.net.MalformedURLException;
030: import java.net.URL;
031: import java.util.ArrayList;
032: import java.util.Enumeration;
033: import java.util.HashMap;
034: import java.util.HashSet;
035: import java.util.Iterator;
036: import java.util.List;
037: import java.util.Map;
038: import java.util.Set;
039: import java.util.jar.JarFile;
040: import java.util.jar.JarInputStream;
041: import java.util.jar.Manifest;
042:
043: import org.jpox.ClassLoaderResolver;
044: import org.jpox.OMFContext;
045: import org.jpox.exceptions.JPOXException;
046: import org.jpox.jdo.AbstractPersistenceManagerFactory;
047: import org.jpox.util.JPOXLogger;
048: import org.jpox.util.Localiser;
049: import org.jpox.util.StringUtils;
050:
051: /**
052: * Manages the registry of Extensions and Extension Points outside any OSGI container.
053: * This implementation cannot handle multiple versions of the same plugin, so it either raises an exception
054: * or logs the issue as a warning. This is different to that mandated by the OSGi specification 3.0 § 3.5.2
055: *
056: * TODO Localise the messages in here.
057: * @version $Revision: 1.26 $
058: */
059: public class NonManagedPluginRegistry implements PluginRegistry {
060: protected static final Localiser LOCALISER = Localiser
061: .getInstance("org.jpox.Localisation");
062:
063: /** ClassLoaderResolver corresponding to the PMF * */
064: private final ClassLoaderResolver clr;
065:
066: /** directories that are searched for plugin files */
067: private static final String PLUGIN_DIR = "/";
068:
069: /** filters all accepted manifest file names */
070: private static final FilenameFilter MANIFEST_FILE_FILTER = new FilenameFilter() {
071: public boolean accept(File dir, String name) {
072: // accept a directory named "meta-inf"
073: if (name.equalsIgnoreCase("meta-inf")) {
074: return true;
075: }
076: // or accept /meta-inf/manifest.mf
077: if (!dir.getName().equalsIgnoreCase("meta-inf")) {
078: return false;
079: }
080: return name.equalsIgnoreCase("manifest.mf");
081: }
082: };
083:
084: /**
085: * Character that is used in URLs of jars to separate the file name from the path of a resource inside
086: * the jar.<br/> example: jar:file:foo.jar!/META-INF/manifest.mf
087: */
088: private static final char JAR_SEPARATOR = '!';
089:
090: /** extension points keyed by Unique Id (plugin.id +"."+ id) * */
091: Map extensionPointsByUniqueId = new HashMap();
092:
093: /** registered bundles files keyed by bundle symbolic name * */
094: Map registeredPluginBypluginId = new HashMap();
095:
096: /** registered bundles files keyed by the manifest.mf url * */
097: Map registeredPluginByManifestURL = new HashMap();
098:
099: /** extension points * */
100: ExtensionPoint[] extensionPoints;
101:
102: private boolean registeredExtensions;
103:
104: /** Type of check on bundles (EXCEPTION, LOG, NONE). */
105: private String bundleCheckType;
106:
107: /**
108: * Constructor
109: * @param clr the ClassLoaderResolver
110: * @param bundleCheckType Type of check on bundles (EXCEPTION, LOG, NONE)
111: */
112: public NonManagedPluginRegistry(ClassLoaderResolver clr,
113: String bundleCheckType) {
114: this .clr = clr;
115: extensionPoints = new ExtensionPoint[0];
116: this .bundleCheckType = bundleCheckType;
117: }
118:
119: /**
120: * Acessor for the ExtensionPoint
121: * @param id the unique id of the extension point
122: * @return null if the ExtensionPoint is not registered
123: */
124: public ExtensionPoint getExtensionPoint(String id) {
125: return (ExtensionPoint) extensionPointsByUniqueId.get(id);
126: }
127:
128: /**
129: * Acessor for the currently registed ExtensionPoints
130: * @return array of ExtensionPoints
131: */
132: public ExtensionPoint[] getExtensionPoints() {
133: return extensionPoints;
134: }
135:
136: /**
137: * Look for Bundles/Plugins and register them. Register also ExtensionPoints and Extensions declared in /plugin.xml
138: * files
139: */
140: public void registerExtensionPoints() {
141: registerExtensions();
142: }
143:
144: /**
145: * Register extension and extension points
146: * @param plugin the URL to the plugin
147: * @param bundle the bundle
148: */
149: public void registerPluginExtensions(URL plugin, Bundle bundle) {
150: // extensions not yet registered
151: List registeringExtensions = new ArrayList();
152: List[] elements = PluginParser.parsePluginElements(this ,
153: plugin, bundle, clr);
154: for (int i = 0; i < elements[0].size(); i++) {
155: ExtensionPoint exPoint = (ExtensionPoint) elements[0]
156: .get(i);
157: extensionPointsByUniqueId.put(exPoint.getUniqueId(),
158: exPoint);
159: }
160: registeringExtensions.addAll(elements[1]);
161: extensionPoints = (ExtensionPoint[]) extensionPointsByUniqueId
162: .values().toArray(
163: new ExtensionPoint[extensionPointsByUniqueId
164: .values().size()]);
165:
166: for (int i = 0; i < registeringExtensions.size(); i++) {
167: Extension extension = (Extension) registeringExtensions
168: .get(i);
169: ExtensionPoint exPoint = getExtensionPoint(extension
170: .getExtensionPointId());
171: if (exPoint == null) {
172: JPOXLogger.PLUGIN.warn(LOCALISER.msg("024002",
173: extension.getExtensionPointId(), extension
174: .getPlugin().getSymbolicName(),
175: extension.getPlugin().getManifestLocation()
176: .toString()));
177: // we just continue processing
178: } else {
179: extension.setExtensionPoint(exPoint);
180: exPoint.addExtension(extension);
181: }
182: }
183: }
184:
185: /**
186: * Look for Bundles/Plugins and register them. Register also ExtensionPoints and Extensions
187: * declared in /plugin.xml files.
188: */
189: public void registerExtensions() {
190: if (registeredExtensions) {
191: return;
192: }
193:
194: // use a set to remove any duplicates
195: Set set = getPluginURLs();
196:
197: // extensions not yet registered
198: List registeringExtensions = new ArrayList();
199:
200: // parse the files (Extensions are automatically added to ExtensionPoint)
201: Iterator it = set.iterator();
202: while (it.hasNext()) {
203: URL plugin = (URL) it.next();
204: URL manifest = getManifestURL(plugin);
205: if (manifest == null) {
206: // No MANIFEST.MF for this plugin.xml so ignore it
207: continue;
208: }
209:
210: Bundle bundle = registerBundle(manifest);
211: if (bundle == null) {
212: // No MANIFEST.MF for this plugin.xml so ignore it
213: continue;
214: }
215: List[] elements = PluginParser.parsePluginElements(this ,
216: plugin, bundle, clr);
217: for (int i = 0; i < elements[0].size(); i++) {
218: ExtensionPoint exPoint = (ExtensionPoint) elements[0]
219: .get(i);
220: extensionPointsByUniqueId.put(exPoint.getUniqueId(),
221: exPoint);
222: }
223: registeringExtensions.addAll(elements[1]);
224: }
225: extensionPoints = (ExtensionPoint[]) extensionPointsByUniqueId
226: .values().toArray(
227: new ExtensionPoint[extensionPointsByUniqueId
228: .values().size()]);
229:
230: for (int i = 0; i < registeringExtensions.size(); i++) {
231: Extension extension = (Extension) registeringExtensions
232: .get(i);
233: ExtensionPoint exPoint = getExtensionPoint(extension
234: .getExtensionPointId());
235: if (exPoint == null) {
236: JPOXLogger.PLUGIN.warn(LOCALISER.msg("024002",
237: extension.getExtensionPointId(), extension
238: .getPlugin().getSymbolicName(),
239: extension.getPlugin().getManifestLocation()
240: .toString()));
241: // we just continue processing
242: } else {
243: extension.setExtensionPoint(exPoint);
244: exPoint.addExtension(extension);
245: }
246: }
247: registeredExtensions = true;
248: }
249:
250: /**
251: * Search and retrieve the URL for the /plugin.xml files located in the classpath
252: * @return a set of {@link URL}
253: */
254: private Set getPluginURLs() {
255: Set set = new HashSet();
256: try {
257: // First add all plugin.xml...
258: Enumeration paths = clr.getResources(PLUGIN_DIR
259: + "plugin.xml",
260: AbstractPersistenceManagerFactory.class
261: .getClassLoader());
262: while (paths.hasMoreElements()) {
263: set.add(paths.nextElement());
264: }
265: } catch (IOException e) {
266: // TODO: Localisation
267: throw new JPOXException("Error loading resource", e)
268: .setFatal();
269: }
270: return set;
271: }
272:
273: /**
274: * Register the plugin bundle
275: * @param manifest the url to the meta-inf/manifest.mf file or a jar file
276: * @return the Plugin
277: */
278: protected Bundle registerBundle(URL manifest) {
279: if (manifest == null) {
280: // TODO Localise this
281: throw new IllegalArgumentException(
282: "Error registering Bundle since URL to manifest.mf is null");
283: }
284:
285: InputStream is = null;
286:
287: try {
288: Manifest mf = null;
289: if (manifest.getProtocol().equals("jar")
290: || manifest.getProtocol().equals("zip")
291: || manifest.getProtocol().equals("wsjar")) {
292: if (manifest.getPath().startsWith("http://")) {
293: // protocol formats:
294: // jar:http:<path>!<manifest-file>, zip:http:<path>!<manifest-file>
295: // e.g jar:http://<host>[:port]/[app-path]/jpox-java5.jar!/plugin.xml
296: JarURLConnection jarConnection = (JarURLConnection) manifest
297: .openConnection();
298: URL url = jarConnection.getJarFileURL();
299: mf = jarConnection.getManifest();
300: if (mf == null) {
301: return null;
302: }
303: return registerBundle(mf, url);
304: } else {
305: // protocol formats:
306: // jar:<path>!<manifest-file>, zip:<path>!<manifest-file>
307: // jar:file:<path>!<manifest-file>, zip:file:<path>!<manifest-file>
308: String path = StringUtils
309: .getDecodedStringFromURLString(manifest
310: .toExternalForm());
311: int index = path.indexOf(JAR_SEPARATOR);
312: String jarPath = path.substring(4, index);
313: if (jarPath.startsWith("file:")) {
314: // remove "file:" from path, so we can use in File constructor
315: jarPath = jarPath.substring(5);
316: }
317: File jarFile = new File(jarPath);
318: mf = new JarFile(jarFile).getManifest();
319: if (mf == null) {
320: return null;
321: }
322: return registerBundle(mf, jarFile.toURI().toURL());
323: }
324: } else if (manifest.getProtocol().equals("rar")
325: || manifest.getProtocol().equals("war")) {
326: // protocol formats:
327: // rar:<rar-path>!<jar-path>!<manifest-file>, war:<war-path>!<jar-path>!<manifest-file>
328: String path = StringUtils
329: .getDecodedStringFromURLString(manifest
330: .toExternalForm());
331: int index = path.indexOf(JAR_SEPARATOR);
332: String rarPath = path.substring(4, index);
333: File file = new File(rarPath);
334: URL rarUrl = file.toURI().toURL();
335:
336: String jarPath = path.substring(index + 1, path
337: .indexOf(JAR_SEPARATOR, index + 1));
338: JarFile rarFile = new JarFile(file);
339: mf = new JarInputStream(rarFile.getInputStream(rarFile
340: .getEntry(jarPath))).getManifest();
341: if (mf == null) {
342: return null;
343: }
344: return registerBundle(mf, rarUrl);
345: } else {
346: is = manifest.openStream();
347: mf = new Manifest(is);
348: return registerBundle(mf, manifest);
349: }
350: } catch (IOException e) {
351: throw new JPOXException("Error reading manifest file", e)
352: .setFatal();
353: } finally {
354: if (is != null) {
355: try {
356: is.close();
357: } catch (IOException e) {
358: // ignored
359: }
360: }
361: }
362: }
363:
364: /**
365: * Register the plugin bundle
366: * @param mf the Manifest
367: * @param manifest the url to the meta-inf/manifest.mf file or a jar file
368: * @return the Plugin
369: */
370: protected Bundle registerBundle(Manifest mf, URL manifest) {
371: Bundle bundle = PluginParser.parseManifest(mf, manifest);
372: if (registeredPluginBypluginId.get(bundle.getSymbolicName()) == null) {
373: if (JPOXLogger.PLUGIN.isDebugEnabled()) {
374: JPOXLogger.PLUGIN.debug("Registering bundle "
375: + bundle.getSymbolicName() + " version "
376: + bundle.getVersion() + " at URL "
377: + bundle.getManifestLocation() + ".");
378: }
379: registeredPluginBypluginId.put(bundle.getSymbolicName(),
380: bundle);
381: registeredPluginByManifestURL.put(bundle
382: .getManifestLocation(), bundle);
383: } else {
384: Bundle previousBundle = (Bundle) registeredPluginBypluginId
385: .get(bundle.getSymbolicName());
386: if (!bundle.getManifestLocation().toExternalForm().equals(
387: previousBundle.getManifestLocation()
388: .toExternalForm())) {
389: // TODO Localise this
390: String msg = "Plugin (Bundle) "
391: + bundle.getSymbolicName()
392: + " is already registered. "
393: + "Ensure you don't have multiple JAR versions of the same plugin in the classpath. "
394: + "The URL "
395: + bundle.getManifestLocation()
396: + " is already registered, "
397: + "and you are trying to register an identical plugin located "
398: + "at URL "
399: + previousBundle.getManifestLocation() + ".";
400: if (bundleCheckType.equalsIgnoreCase("EXCEPTION")) {
401: throw new JPOXException(msg);
402: } else if (bundleCheckType.equalsIgnoreCase("LOG")) {
403: JPOXLogger.PLUGIN.warn(msg);
404: } else {
405: // Nothing
406: }
407: }
408: }
409: return bundle;
410: }
411:
412: /**
413: * Get the URL to the manifest.mf file relative to the plugin URL ($pluginurl/meta-inf/manifest.mf)
414: * @param plugin the url to the plugin.xml file
415: * @return a URL to the manifest.mf file or a URL for a jar file
416: */
417: private URL getManifestURL(URL plugin) {
418: if (plugin == null) {
419: return null;
420: }
421: if (plugin.toString().startsWith("jar")
422: || plugin.toString().startsWith("zip")
423: || plugin.toString().startsWith("rar")
424: || plugin.toString().startsWith("war")
425: || plugin.toString().startsWith("wsjar")) {
426: // URL for file containing the manifest
427: return plugin;
428: } else if (plugin.toString().startsWith("jndi")) {
429: // "Oracle AS" uses JNDI protocol. For example
430: // input: jndi:/opt/oracle/product/10.1.3.0.3_portal/j2ee/OC4J_Portal/applications/presto/presto/WEB-INF/lib/jpox-rdbms-1.2-SNAPSHOT.jar/plugin.xml
431: // output: jar:file:/opt/oracle/product/10.1.3.0.3_portal/j2ee/OC4J_Portal/applications/presto/presto/WEB-INF/lib/jpox-rdbms-1.2-SNAPSHOT.jar!/plugin.xml
432: String urlStr = plugin.toString().substring(5);
433: urlStr = urlStr.replaceAll("\\.jar/", ".jar!/");
434: urlStr = "jar:file:" + urlStr;
435: try {
436: // URL for file containing the manifest
437: return new URL(urlStr);
438: } catch (MalformedURLException e) {
439: JPOXLogger.PLUGIN.warn(
440: "Error creating URL for plugin MANIFEST file "
441: + urlStr, e);
442: return null;
443: }
444: }
445:
446: File file = new File(plugin.getFile());
447: File[] dirs = new File(file.getParent())
448: .listFiles(MANIFEST_FILE_FILTER);
449: if (dirs != null && dirs.length > 0) {
450: File[] files = dirs[0].listFiles(MANIFEST_FILE_FILTER);
451: if (files != null && files.length > 0) {
452: try {
453: return files[0].toURI().toURL();
454: } catch (MalformedURLException e) {
455: JPOXLogger.PLUGIN.warn(
456: "Error reading MANIFEST.MF for " + plugin,
457: e);
458: return null;
459: }
460: }
461: }
462: // TODO Localise this
463: JPOXLogger.PLUGIN
464: .warn("Could not find MANIFEST.MF file for plugin file "
465: + plugin + " so ignoring it");
466: return null;
467: }
468:
469: /**
470: * Loads a class (do not initialize) from an attribute of {@link ConfigurationElement}
471: * @param confElm the configuration element
472: * @param name the attribute name
473: * @return the Class
474: */
475: public Object createExecutableExtension(
476: ConfigurationElement confElm, String name,
477: Class[] argsClass, Object[] args)
478: throws ClassNotFoundException, SecurityException,
479: NoSuchMethodException, IllegalArgumentException,
480: InstantiationException, IllegalAccessException,
481: InvocationTargetException {
482: Class cls = clr.classForName(confElm.getAttribute(name),
483: OMFContext.class.getClassLoader());
484: Constructor constructor = cls.getConstructor(argsClass);
485: return constructor.newInstance(args);
486: }
487:
488: /**
489: * Loads a class (do not initialize)
490: * @param pluginId the plugin id
491: * @param className the class name
492: * @return the Class
493: * @throws ClassNotFoundException
494: */
495: public Class loadClass(String pluginId, String className)
496: throws ClassNotFoundException {
497: return clr.classForName(className, OMFContext.class
498: .getClassLoader());
499: }
500:
501: /**
502: * Converts a URL that uses a user-defined protocol into a URL that uses the file protocol.
503: * @param url the url to be converted
504: * @return the converted URL
505: * @throws IOException
506: */
507: public URL resolveURLAsFileURL(URL url) throws IOException {
508: return url;
509: }
510:
511: /**
512: * Resolve constraints declared in bundle manifest.mf files.
513: * This must be invoked after registering all bundles.
514: * Should log errors if bundles are not resolvable, or raise runtime exceptions.
515: */
516: public void resolveConstraints() {
517: Iterator it = registeredPluginBypluginId.values().iterator();
518: while (it.hasNext()) {
519: Bundle bundle = (Bundle) it.next();
520: List set = bundle.getRequireBundle();
521: Iterator requiredBundles = set.iterator();
522: while (requiredBundles.hasNext()) {
523: Bundle.BundleDescription bd = (Bundle.BundleDescription) requiredBundles
524: .next();
525: String symbolicName = bd.getBundleSymbolicName();
526:
527: Bundle requiredBundle = (Bundle) registeredPluginBypluginId
528: .get(symbolicName);
529: if (requiredBundle == null) {
530: if (bd.getParameter("resolution") != null
531: && bd.getParameter("resolution")
532: .equalsIgnoreCase("optional")) {
533: // TODO Localise this
534: JPOXLogger.PLUGIN.warn("Bundle "
535: + bundle.getSymbolicName()
536: + " has an optional dependency to "
537: + symbolicName
538: + " but it cannot be resolved.");
539: } else {
540: // TODO Localise this
541: JPOXLogger.PLUGIN.error("Bundle "
542: + bundle.getSymbolicName()
543: + " requires " + symbolicName
544: + " but it cannot be resolved.");
545: }
546: }
547: if (bd.getParameter("bundle-version") != null) {
548: if (!isVersionInInterval(bundle.getVersion(), bd
549: .getParameter("bundle-version"))) {
550: // TODO Localise this
551: JPOXLogger.PLUGIN
552: .error("Bundle "
553: + bundle.getSymbolicName()
554: + " requires "
555: + symbolicName
556: + " version "
557: + bd
558: .getParameter("bundle-version")
559: + " but the resolved bundle has version "
560: + bundle.getVersion()
561: + " which is outside the expected range.");
562: }
563: }
564: }
565: }
566: }
567:
568: /**
569: * Check if the version is in interval
570: * @param version
571: * @param interval
572: * @return
573: */
574: private boolean isVersionInInterval(String version, String interval) {
575: //versionRange has only floor
576: Bundle.BundleVersionRange versionRange = PluginParser
577: .parseVersionRange(version);
578: Bundle.BundleVersionRange intervalRange = PluginParser
579: .parseVersionRange(interval);
580: int compare_floor = versionRange.floor
581: .compareTo(intervalRange.floor);
582: boolean result = true;
583: if (intervalRange.floor_inclusive) {
584: result = compare_floor >= 0;
585: } else {
586: result = compare_floor > 0;
587: }
588: if (intervalRange.ceiling != null) {
589: int compare_ceiling = versionRange.floor
590: .compareTo(intervalRange.ceiling);
591: if (intervalRange.ceiling_inclusive) {
592: result = compare_ceiling <= 0;
593: } else {
594: result = compare_ceiling < 0;
595: }
596: }
597: return result;
598: }
599:
600: /**
601: * Accessor for all registered bundles
602: * @return the bundles
603: * @throws UnsupportedOperationException if this operation is not supported by the implementation
604: */
605: public Bundle[] getBundles() {
606: return (Bundle[]) registeredPluginBypluginId.values().toArray(
607: new Bundle[registeredPluginBypluginId.values().size()]);
608: }
609: }
|