/**********************************************************************
Copyright (c) 2006 Erik Bengtson and others. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Contributors:
2006 Thomas Marti - Added support for configurable plugin file names
2007 Andr� F�genschuh - Support for protocol "jar:http:"
2008 Alexi Polenur - Support for Oracle AS "jndi:", "code-source:" protocols
...
**********************************************************************/
package org.jpox.plugin;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import org.jpox.ClassLoaderResolver;
import org.jpox.OMFContext;
import org.jpox.exceptions.JPOXException;
import org.jpox.jdo.AbstractPersistenceManagerFactory;
import org.jpox.util.JPOXLogger;
import org.jpox.util.Localiser;
import org.jpox.util.StringUtils;
/**
* Manages the registry of Extensions and Extension Points outside any OSGI container.
* This implementation cannot handle multiple versions of the same plugin, so it either raises an exception
* or logs the issue as a warning. This is different to that mandated by the OSGi specification 3.0 � 3.5.2
*/
public class NonManagedPluginRegistry implements PluginRegistry
{
protected static final Localiser LOCALISER = Localiser.getInstance("org.jpox.Localisation",
OMFContext.class.getClassLoader());
/** ClassLoaderResolver corresponding to the PMF * */
private final ClassLoaderResolver clr;
/** directories that are searched for plugin files */
private static final String PLUGIN_DIR = "/";
/** filters all accepted manifest file names */
private static final FilenameFilter MANIFEST_FILE_FILTER = new FilenameFilter()
{
public boolean accept(File dir, String name)
{
// accept a directory named "meta-inf"
if (name.equalsIgnoreCase("meta-inf"))
{
return true;
}
// or accept /meta-inf/manifest.mf
if (!dir.getName().equalsIgnoreCase("meta-inf"))
{
return false;
}
return name.equalsIgnoreCase("manifest.mf");
}
};
/**
* Character that is used in URLs of jars to separate the file name from the path of a resource inside
* the jar.<br/> example: jar:file:foo.jar!/META-INF/manifest.mf
*/
private static final char JAR_SEPARATOR = '!';
/** extension points keyed by Unique Id (plugin.id +"."+ id) * */
Map extensionPointsByUniqueId = new HashMap();
/** registered bundles files keyed by bundle symbolic name * */
Map registeredPluginBypluginId = new HashMap();
/** registered bundles files keyed by the manifest.mf url * */
Map registeredPluginByManifestURL = new HashMap();
/** extension points * */
ExtensionPoint[] extensionPoints;
private boolean registeredExtensions;
/** Type of check on bundles (EXCEPTION, LOG, NONE). */
private String bundleCheckType;
/**
* Constructor
* @param clr the ClassLoaderResolver
* @param bundleCheckType Type of check on bundles (EXCEPTION, LOG, NONE)
*/
public NonManagedPluginRegistry(ClassLoaderResolver clr, String bundleCheckType)
{
this.clr = clr;
extensionPoints = new ExtensionPoint[0];
if (bundleCheckType == null)
{
//we must check for nulls, and set the default here, because default values are not yet loaded
this.bundleCheckType = "EXCEPTION";
}
else
{
this.bundleCheckType = bundleCheckType;
}
}
/**
* Acessor for the ExtensionPoint
* @param id the unique id of the extension point
* @return null if the ExtensionPoint is not registered
*/
public ExtensionPoint getExtensionPoint(String id)
{
return (ExtensionPoint) extensionPointsByUniqueId.get(id);
}
/**
* Acessor for the currently registed ExtensionPoints
* @return array of ExtensionPoints
*/
public ExtensionPoint[] getExtensionPoints()
{
return extensionPoints;
}
/**
* Look for Bundles/Plugins and register them. Register also ExtensionPoints and Extensions declared in /plugin.xml
* files
*/
public void registerExtensionPoints()
{
registerExtensions();
}
/**
* Register extension and extension points
* @param plugin the URL to the plugin
* @param bundle the bundle
*/
public void registerPluginExtensions(URL plugin, Bundle bundle)
{
// extensions not yet registered
List registeringExtensions = new ArrayList();
List[] elements = PluginParser.parsePluginElements(this, plugin, bundle, clr);
for (int i = 0; i < elements[0].size(); i++)
{
ExtensionPoint exPoint = (ExtensionPoint) elements[0].get(i);
extensionPointsByUniqueId.put(exPoint.getUniqueId(), exPoint);
}
registeringExtensions.addAll(elements[1]);
extensionPoints = (ExtensionPoint[]) extensionPointsByUniqueId.values().toArray(
new ExtensionPoint[extensionPointsByUniqueId.values().size()]);
for (int i = 0; i < registeringExtensions.size(); i++)
{
Extension extension = (Extension)registeringExtensions.get(i);
ExtensionPoint exPoint = getExtensionPoint(extension.getExtensionPointId());
if (exPoint == null)
{
JPOXLogger.PLUGIN.warn(LOCALISER.msg("024002", extension.getExtensionPointId(), extension.getPlugin().getSymbolicName(), extension.getPlugin().getManifestLocation().toString()));
// we just continue processing
}
else
{
extension.setExtensionPoint(exPoint);
exPoint.addExtension(extension);
}
}
}
/**
* Look for Bundles/Plugins and register them. Register also ExtensionPoints and Extensions
* declared in /plugin.xml files.
*/
public void registerExtensions()
{
if (registeredExtensions)
{
return;
}
// use a set to remove any duplicates
Set set = getPluginURLs();
// extensions not yet registered
List registeringExtensions = new ArrayList();
// parse the files (Extensions are automatically added to ExtensionPoint)
Iterator it = set.iterator();
while (it.hasNext())
{
URL plugin = (URL) it.next();
URL manifest = getManifestURL(plugin);
if (manifest == null)
{
// No MANIFEST.MF for this plugin.xml so ignore it
continue;
}
Bundle bundle = registerBundle(manifest);
if (bundle == null)
{
// No MANIFEST.MF for this plugin.xml so ignore it
continue;
}
List[] elements = PluginParser.parsePluginElements(this, plugin, bundle, clr);
for (int i = 0; i < elements[0].size(); i++)
{
ExtensionPoint exPoint = (ExtensionPoint) elements[0].get(i);
extensionPointsByUniqueId.put(exPoint.getUniqueId(), exPoint);
}
registeringExtensions.addAll(elements[1]);
}
extensionPoints = (ExtensionPoint[]) extensionPointsByUniqueId.values().toArray(
new ExtensionPoint[extensionPointsByUniqueId.values().size()]);
for (int i = 0; i < registeringExtensions.size(); i++)
{
Extension extension = (Extension)registeringExtensions.get(i);
ExtensionPoint exPoint = getExtensionPoint(extension.getExtensionPointId());
if (exPoint == null)
{
JPOXLogger.PLUGIN.warn(LOCALISER.msg("024002", extension.getExtensionPointId(), extension.getPlugin().getSymbolicName(), extension.getPlugin().getManifestLocation().toString()));
// we just continue processing
}
else
{
extension.setExtensionPoint(exPoint);
exPoint.addExtension(extension);
}
}
registeredExtensions=true;
}
/**
* Search and retrieve the URL for the /plugin.xml files located in the classpath
* @return a set of {@link URL}
*/
private Set getPluginURLs()
{
Set set = new HashSet();
try
{
// First add all plugin.xml...
Enumeration paths = clr.getResources(PLUGIN_DIR + "plugin.xml", AbstractPersistenceManagerFactory.class.getClassLoader());
while (paths.hasMoreElements())
{
set.add(paths.nextElement());
}
}
catch (IOException e)
{
throw new JPOXException("Error loading resource", e).setFatal();
}
return set;
}
/**
* Register the plugin bundle
* @param manifest the url to the meta-inf/manifest.mf file or a jar file
* @return the Plugin
*/
protected Bundle registerBundle(URL manifest)
{
if (manifest == null)
{
throw new IllegalArgumentException(LOCALISER.msg("024007"));
}
InputStream is = null;
try
{
Manifest mf = null;
if (manifest.getProtocol().equals("jar") || manifest.getProtocol().equals("zip") ||
manifest.getProtocol().equals("wsjar"))
{
if (manifest.getPath().startsWith("http://"))
{
// protocol formats:
// jar:http:<path>!<manifest-file>, zip:http:<path>!<manifest-file>
// e.g jar:http://<host>[:port]/[app-path]/jpox-java5.jar!/plugin.xml
JarURLConnection jarConnection = (JarURLConnection) manifest.openConnection();
URL url = jarConnection.getJarFileURL();
mf = jarConnection.getManifest();
if (mf == null)
{
return null;
}
return registerBundle(mf, url);
}
else
{
int begin = 4;
if (manifest.getProtocol().equals("wsjar"))
{
begin = 6;
}
// protocol formats:
// jar:<path>!<manifest-file>, zip:<path>!<manifest-file>
// jar:file:<path>!<manifest-file>, zip:file:<path>!<manifest-file>
String path = StringUtils.getDecodedStringFromURLString(manifest.toExternalForm());
int index = path.indexOf(JAR_SEPARATOR);
String jarPath = path.substring(begin, index);
if (jarPath.startsWith("file:"))
{
// remove "file:" from path, so we can use in File constructor
jarPath = jarPath.substring(5);
}
File jarFile = new File(jarPath);
mf = new JarFile(jarFile).getManifest();
if (mf == null)
{
return null;
}
return registerBundle(mf, jarFile.toURI().toURL());
}
}
else if (manifest.getProtocol().equals("rar") || manifest.getProtocol().equals("war"))
{
// protocol formats:
// rar:<rar-path>!<jar-path>!<manifest-file>, war:<war-path>!<jar-path>!<manifest-file>
String path = StringUtils.getDecodedStringFromURLString(manifest.toExternalForm());
int index = path.indexOf(JAR_SEPARATOR);
String rarPath = path.substring(4, index);
File file = new File(rarPath);
URL rarUrl = file.toURI().toURL();
String jarPath = path.substring(index+1, path.indexOf(JAR_SEPARATOR,index+1));
JarFile rarFile = new JarFile(file);
mf = new JarInputStream(rarFile.getInputStream(rarFile.getEntry(jarPath))).getManifest();
if (mf == null)
{
return null;
}
return registerBundle(mf, rarUrl);
}
else
{
is = manifest.openStream();
mf = new Manifest(is);
return registerBundle(mf,manifest);
}
}
catch (IOException e)
{
throw new JPOXException(LOCALISER.msg("024008", manifest), e).setFatal();
}
finally
{
if (is != null)
{
try
{
is.close();
}
catch (IOException e)
{
// ignored
}
}
}
}
/**
* Register the plugin bundle
* @param mf the Manifest
* @param manifest the url to the meta-inf/manifest.mf file or a jar file
* @return the Plugin
*/
protected Bundle registerBundle(Manifest mf, URL manifest)
{
Bundle bundle = PluginParser.parseManifest(mf, manifest);
if (registeredPluginBypluginId.get(bundle.getSymbolicName()) == null)
{
if (JPOXLogger.PLUGIN.isDebugEnabled())
{
JPOXLogger.PLUGIN.debug("Registering bundle "+bundle.getSymbolicName()+" version "+bundle.getVersion()+" at URL "+bundle.getManifestLocation()+".");
}
registeredPluginBypluginId.put(bundle.getSymbolicName(), bundle);
registeredPluginByManifestURL.put(bundle.getManifestLocation(), bundle);
}
else
{
Bundle previousBundle = (Bundle)registeredPluginBypluginId.get(bundle.getSymbolicName());
if (!bundle.getManifestLocation().toExternalForm().equals(previousBundle.getManifestLocation().toExternalForm()))
{
String msg = LOCALISER.msg("024009", bundle.getSymbolicName(),
bundle.getManifestLocation(), previousBundle.getManifestLocation());
if (bundleCheckType.equalsIgnoreCase("EXCEPTION"))
{
throw new JPOXException(msg);
}
else if (bundleCheckType.equalsIgnoreCase("LOG"))
{
JPOXLogger.PLUGIN.warn(msg);
}
else
{
// Nothing
}
}
}
return bundle;
}
/**
* Get the URL to the manifest.mf file relative to the plugin URL ($pluginurl/meta-inf/manifest.mf)
* @param plugin the url to the plugin.xml file
* @return a URL to the manifest.mf file or a URL for a jar file
*/
private URL getManifestURL(URL plugin)
{
if (plugin == null)
{
return null;
}
if (plugin.toString().startsWith("jar") || plugin.toString().startsWith("zip") ||
plugin.toString().startsWith("rar") || plugin.toString().startsWith("war") ||
plugin.toString().startsWith("wsjar"))
{
// URL for file containing the manifest
return plugin;
}
else if (plugin.toString().startsWith("jndi"))
{
// "Oracle AS" uses JNDI protocol. For example
// 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
// 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
String urlStr = plugin.toString().substring(5);
urlStr = urlStr.replaceAll("\\.jar/", ".jar!/");
urlStr = "jar:file:" + urlStr;
try
{
// URL for file containing the manifest
return new URL(urlStr);
}
catch (MalformedURLException e)
{
JPOXLogger.PLUGIN.warn(LOCALISER.msg("024010", urlStr), e);
return null;
}
}
else if (plugin.toString().startsWith("code-source"))
{
// "Oracle AS" also uses code-source protocol. For example
// input: code-source:/opt/oc4j/j2ee/home/applications/presto/presto/WEB-INF/lib/jpox-rdmbs-1.2-SNAPSHOT.jar!/plugin.xml
// output: jar:file:/opt/oc4j/j2ee/home/applications/presto/presto/WEB-INF/lib/jpox-rdmbs-1.2-SNAPSHOT.jar!/plugin.xml
String urlStr = plugin.toString().substring(12); //strip "code-source:"
urlStr = "jar:file:" + urlStr;
try
{
// URL for file containing the manifest
return new URL(urlStr);
}
catch (MalformedURLException e)
{
JPOXLogger.PLUGIN.warn(LOCALISER.msg("024010", urlStr), e);
return null;
}
}
try
{
File file = new File(new URI(plugin.toString()).getPath());
File[] dirs = new File(file.getParent()).listFiles(MANIFEST_FILE_FILTER);
if (dirs != null && dirs.length > 0)
{
File[] files = dirs[0].listFiles(MANIFEST_FILE_FILTER);
if (files != null && files.length > 0)
{
try
{
return files[0].toURI().toURL();
}
catch (MalformedURLException e)
{
JPOXLogger.PLUGIN.warn(LOCALISER.msg("024011", plugin), e);
return null;
}
}
}
}
catch (URISyntaxException use)
{
use.printStackTrace();
JPOXLogger.PLUGIN.warn(LOCALISER.msg("024011", plugin), use);
return null;
}
JPOXLogger.PLUGIN.warn(LOCALISER.msg("024012", plugin));
return null;
}
/**
* Loads a class (do not initialize) from an attribute of {@link ConfigurationElement}
* @param confElm the configuration element
* @param name the attribute name
* @return the Class
*/
public Object createExecutableExtension(ConfigurationElement confElm, String name, Class[] argsClass, Object[] args)
throws ClassNotFoundException,
SecurityException,
NoSuchMethodException,
IllegalArgumentException,
InstantiationException,
IllegalAccessException,
InvocationTargetException
{
Class cls = clr.classForName(confElm.getAttribute(name),OMFContext.class.getClassLoader());
Constructor constructor = cls.getConstructor(argsClass);
return constructor.newInstance(args);
}
/**
* Loads a class (do not initialize)
* @param pluginId the plugin id
* @param className the class name
* @return the Class
* @throws ClassNotFoundException
*/
public Class loadClass(String pluginId, String className) throws ClassNotFoundException
{
return clr.classForName(className, OMFContext.class.getClassLoader());
}
/**
* Converts a URL that uses a user-defined protocol into a URL that uses the file protocol.
* @param url the url to be converted
* @return the converted URL
* @throws IOException
*/
public URL resolveURLAsFileURL(URL url) throws IOException
{
return url;
}
/**
* Resolve constraints declared in bundle manifest.mf files.
* This must be invoked after registering all bundles.
* Should log errors if bundles are not resolvable, or raise runtime exceptions.
*/
public void resolveConstraints()
{
Iterator it = registeredPluginBypluginId.values().iterator();
while (it.hasNext())
{
Bundle bundle = (Bundle)it.next();
List set = bundle.getRequireBundle();
Iterator requiredBundles = set.iterator();
while (requiredBundles.hasNext())
{
Bundle.BundleDescription bd = (Bundle.BundleDescription) requiredBundles.next();
String symbolicName = bd.getBundleSymbolicName();
Bundle requiredBundle = (Bundle) registeredPluginBypluginId.get(symbolicName);
if (requiredBundle == null)
{
if (bd.getParameter("resolution") != null &&
bd.getParameter("resolution").equalsIgnoreCase("optional"))
{
JPOXLogger.PLUGIN.warn(LOCALISER.msg("024013", bundle.getSymbolicName(), symbolicName));
}
else
{
JPOXLogger.PLUGIN.error(LOCALISER.msg("024014", bundle.getSymbolicName(), symbolicName));
}
}
if (bd.getParameter("bundle-version") != null)
{
if (requiredBundle != null &&
!isVersionInInterval(requiredBundle.getVersion(), bd.getParameter("bundle-version")))
{
JPOXLogger.PLUGIN.error(LOCALISER.msg("024015", bundle.getSymbolicName(),
symbolicName, bd.getParameter("bundle-version"), bundle.getVersion()));
}
}
}
}
}
/**
* Check if the version is in interval
* @param version
* @param interval
* @return
*/
private boolean isVersionInInterval(String version, String interval)
{
//versionRange has only floor
Bundle.BundleVersionRange versionRange = PluginParser.parseVersionRange(version);
Bundle.BundleVersionRange intervalRange = PluginParser.parseVersionRange(interval);
int compare_floor=versionRange.floor.compareTo(intervalRange.floor);
boolean result = true;
if (intervalRange.floor_inclusive)
{
result = compare_floor >= 0;
}
else
{
result = compare_floor > 0;
}
if (intervalRange.ceiling != null)
{
int compare_ceiling = versionRange.floor.compareTo(intervalRange.ceiling);
if (intervalRange.ceiling_inclusive)
{
result = compare_ceiling <= 0;
}
else
{
result = compare_ceiling<0;
}
}
return result;
}
/**
* Accessor for all registered bundles
* @return the bundles
* @throws UnsupportedOperationException if this operation is not supported by the implementation
*/
public Bundle[] getBundles()
{
return (Bundle[])registeredPluginBypluginId.values().toArray(new Bundle[registeredPluginBypluginId.values().size()]);
}
}