package org.atricore.idbus.bundles.datanucleus.core;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.datanucleus.ClassLoaderResolver;
import org.datanucleus.OMFContext;
import org.datanucleus.exceptions.NucleusException;
import org.datanucleus.jdo.JDOPersistenceManagerFactory;
import org.datanucleus.plugin.*;
import org.datanucleus.util.Localiser;
import org.datanucleus.util.NucleusLogger;
import org.datanucleus.util.StringUtils;
import javax.xml.parsers.DocumentBuilder;
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.*;
import java.util.*;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
/**
* @author <a href="mailto:sgonzalez@atricore.org">Sebastian Gonzalez Oyuela</a>
* @version $Id$
*/
public class OsgiPluginRegistry implements PluginRegistry {
private static final Log logger = LogFactory.getLog(OsgiPluginRegistry.class);
protected static final Localiser LOCALISER = Localiser.getInstance("org.datanucleus.Localisation",
OMFContext.class.getClassLoader());
/** DataNucleus package to define whether to check for deps, etc. */
private static final String DATANUCLEUS_PKG = "org.datanucleus";
/** 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 = '!';
/** ClassLoaderResolver corresponding to the persistence context. */
private final ClassLoaderResolver clr;
/** extension points keyed by Unique Id (plugin.id +"."+ id) * */
Map<String, ExtensionPoint> extensionPointsByUniqueId = new HashMap();
/** registered bundles files keyed by bundle symbolic name * */
Map<String, Bundle> registeredPluginByPluginId = new HashMap();
/** extension points * */
ExtensionPoint[] extensionPoints;
private boolean registeredExtensions;
/** Type of check on bundles (EXCEPTION, LOG, NONE). */
private String bundleCheckType = "EXCEPTION";
/**
* Constructor.
* @param clr the ClassLoaderResolver
*/
public OsgiPluginRegistry(ClassLoaderResolver clr)
{
logger.debug("Initializing Datanucleus OSGi Plugin Registry");
this.clr = clr;
extensionPoints = new ExtensionPoint[0];
}
/**
* Accessor for the ExtensionPoint with the specified id.
* @param id the unique id of the extension point
* @return null if the ExtensionPoint is not registered
*/
public ExtensionPoint getExtensionPoint(String id)
{
return extensionPointsByUniqueId.get(id);
}
/**
* Accessor for the currently registered ExtensionPoints.
* @return array of ExtensionPoints
*/
public ExtensionPoint[] getExtensionPoints()
{
return extensionPoints;
}
/**
* Look for Bundles/Plugins and registration them. Register also ExtensionPoints and Extensions declared in /plugin.xml
* files
*/
public void registerExtensionPoints()
{
registerExtensions();
}
/**
* Register extension and extension points for the specified plugin.
* @param pluginURL the URL to the plugin
* @param bundle the bundle
*/
public void registerExtensionsForPlugin(URL pluginURL, Bundle bundle)
{
DocumentBuilder docBuilder = PluginParser.getDocumentBuilder();
List[] elements = PluginParser.parsePluginElements(docBuilder, this, pluginURL, bundle, clr);
registerExtensionPointsForPluginInternal(elements[0], true);
// Register extensions
Iterator<Extension> pluginExtensionIter = elements[1].iterator();
while (pluginExtensionIter.hasNext())
{
Extension extension = pluginExtensionIter.next();
ExtensionPoint exPoint = extensionPointsByUniqueId.get(extension.getExtensionPointId());
if (exPoint == null)
{
NucleusLogger.PLUGIN.warn(LOCALISER.msg("024002", extension.getExtensionPointId(),
extension.getPlugin().getSymbolicName(), extension.getPlugin().getManifestLocation().toString()));
}
else
{
extension.setExtensionPoint(exPoint);
exPoint.addExtension(extension);
}
}
}
/**
* Look for Bundles/Plugins and registration them.
* Register also ExtensionPoints and Extensions declared in "/plugin.xml" files.
*/
public void registerExtensions()
{
if (logger.isDebugEnabled())
logger.debug("registerextensions:" + registeredExtensions);
if (registeredExtensions)
{
return;
}
List registeringExtensions = new ArrayList();
// parse the plugin files
DocumentBuilder docBuilder = PluginParser.getDocumentBuilder();
Set<URL> urls = getPluginURLs();
if (logger.isDebugEnabled())
logger.debug("Using plugin urls " + urls.size());
Iterator<URL> it = urls.iterator();
while (it.hasNext())
{
URL pluginURL = it.next();
URL manifest = getManifestURL(pluginURL);
if (logger.isDebugEnabled())
logger.debug("Plugin URLs " + pluginURL + "/" + manifest);
if (manifest == null)
{
if (logger.isDebugEnabled())
logger.debug("No MANIFEST.MF for this plugin.xml so ignore it (1)");
// No MANIFEST.MF for this plugin.xml so ignore it
continue;
}
Bundle bundle = registerBundle(manifest);
if (bundle == null)
{
if (logger.isDebugEnabled())
logger.debug("No MANIFEST.MF for this plugin.xml so ignore it (2)");
// No MANIFEST.MF for this plugin.xml so ignore it
continue;
}
if (logger.isDebugEnabled())
logger.debug("Accepted bundle with plugins: " + bundle.getSymbolicName());
List[] elements =
PluginParser.parsePluginElements(docBuilder, this, pluginURL, bundle, clr);
if (logger.isDebugEnabled())
logger.debug("Registering extension points for " + pluginURL + " with configuration elements " + elements.length);
registerExtensionPointsForPluginInternal(elements[0], false);
registeringExtensions.addAll(elements[1]);
}
extensionPoints = extensionPointsByUniqueId.values().toArray(
new ExtensionPoint[extensionPointsByUniqueId.values().size()]);
if (logger.isDebugEnabled())
logger.debug("Registered extension points ok, now registering extensions");
// Register the extensions now that we have the extension-points all loaded
for (int i = 0; i < registeringExtensions.size(); i++)
{
Extension extension = (Extension)registeringExtensions.get(i);
ExtensionPoint exPoint = getExtensionPoint(extension.getExtensionPointId());
if (logger.isDebugEnabled())
logger.debug("ExtensionPoint:" + (exPoint != null ? exPoint.getId() : null)+ " has extension with " +
extension.getConfigurationElements().length + " configuration elements");
if (exPoint == null)
{
if (extension.getPlugin().getSymbolicName().startsWith(DATANUCLEUS_PKG))
{
NucleusLogger.PLUGIN.warn(LOCALISER.msg("024002", extension.getExtensionPointId(),
extension.getPlugin().getSymbolicName(), extension.getPlugin().getManifestLocation().toString()));
}
}
else
{
extension.setExtensionPoint(exPoint);
exPoint.addExtension(extension);
}
}
registeredExtensions = true;
}
/**
* Register extension-points for the specified plugin.
* @param extPoints ExtensionPoints for this plugin
* @param updateExtensionPointsArray Whether to update "extensionPoints" array
*/
protected void registerExtensionPointsForPluginInternal(List extPoints, boolean updateExtensionPointsArray)
{
// Register extension-points
Iterator<ExtensionPoint> pluginExtPointIter = extPoints.iterator();
while (pluginExtPointIter.hasNext())
{
ExtensionPoint exPoint = pluginExtPointIter.next();
extensionPointsByUniqueId.put(exPoint.getUniqueId(), exPoint);
}
if (updateExtensionPointsArray)
{
extensionPoints = extensionPointsByUniqueId.values().toArray(
new ExtensionPoint[extensionPointsByUniqueId.values().size()]);
}
}
/**
* Search and retrieve the URL for the "/plugin.xml" files located in the classpath.
* @return a set of {@link java.net.URL}
*/
private Set<URL> getPluginURLs()
{
Set<URL> set = new HashSet();
try
{
// TODO : Make configurable ;)
String[] pluginPaths = { PLUGIN_DIR + "plugin.xml", "META-INF" + PLUGIN_DIR + "plugin.xml"} ;
for (int i = 0; i < pluginPaths.length; i++) {
String pluginPath = pluginPaths[i];
if(logger.isDebugEnabled())
logger.debug("Looking for plugins " + pluginPath + "plugin.xml, using clr " + clr);
Enumeration<URL> paths =
clr.getResources(pluginPath, JDOPersistenceManagerFactory.class.getClassLoader());
while (paths.hasMoreElements()) {
URL u = paths.nextElement();
if (logger.isDebugEnabled())
logger.debug("Found plugin resource " + u.toExternalForm());
set.add(u);
}
}
}
catch (IOException e)
{
logger.debug("Error loading resource : "+ PLUGIN_DIR + "plugin.xml");
throw new NucleusException("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 if (manifest.getProtocol().equals("vfsfile") || manifest.getProtocol().equals("vfsjar") ||
manifest.getProtocol().equals("vfszip"))
{
// protocol formats:
// vfsfile:<path>!<manifest-file>, vfsjar:<path>!<manifest-file>, vfszip:<path>!<manifest-file>
String path = StringUtils.getDecodedStringFromURLString(manifest.toExternalForm());
int index = path.indexOf(JAR_SEPARATOR);
String jarPath = path.substring(0, index);
URL jarUrl = new URL(jarPath);
JarInputStream jis = new JarInputStream(jarUrl.openConnection().getInputStream());
mf = jis.getManifest();
if (mf == null)
{
return null;
}
return registerBundle(mf, jarUrl);
}
else
{
is = manifest.openStream();
mf = new Manifest(is);
return registerBundle(mf,manifest);
}
}
catch (IOException e)
{
throw new NucleusException(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 (NucleusLogger.PLUGIN.isDebugEnabled())
{
NucleusLogger.PLUGIN.debug("Registering bundle " + bundle.getSymbolicName() + " version " +
bundle.getVersion() + " at URL " + bundle.getManifestLocation() + ".");
}
registeredPluginByPluginId.put(bundle.getSymbolicName(), bundle);
}
else
{
Bundle previousBundle = registeredPluginByPluginId.get(bundle.getSymbolicName());
if (bundle.getSymbolicName().startsWith(DATANUCLEUS_PKG) &&
!bundle.getManifestLocation().toExternalForm().equals(previousBundle.getManifestLocation().toExternalForm()))
{
String msg = LOCALISER.msg("024009", bundle.getSymbolicName(),
bundle.getManifestLocation(), previousBundle.getManifestLocation());
if (bundleCheckType.equalsIgnoreCase("EXCEPTION"))
{
throw new NucleusException(msg);
}
else if (bundleCheckType.equalsIgnoreCase("LOG"))
{
NucleusLogger.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 pluginURL 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 pluginURL)
{
if (pluginURL == null)
{
return null;
}
if (pluginURL.toString().startsWith("jar") || pluginURL.toString().startsWith("zip") ||
pluginURL.toString().startsWith("rar") || pluginURL.toString().startsWith("war") ||
pluginURL.toString().startsWith("wsjar"))
{
// URL for file containing the manifest
return pluginURL;
}
else if (pluginURL.toString().startsWith("vfsfile") || pluginURL.toString().startsWith("vfsjar") ||
pluginURL.toString().startsWith("vfszip"))
{
// JBoss (5+) proprietary protocols input:
// vfsfile:C:/appserver/jboss-5.0.0.Beta4/server/default/deploy/datanucleus-jca-1.0.0.rar/datanucleus-core-1.0-SNAPSHOT.jar/plugin.xml
// output:
// vfsfile:C:/appserver/jboss-5.0.0.Beta4/server/default/deploy/datanucleus-jca-1.0.0.rar/datanucleus-core-1.0-SNAPSHOT.jar!/plugin.xml
String urlStr = pluginURL.toString().replaceAll("\\.jar/", ".jar!/");
try
{
return new URL(urlStr);
}
catch (MalformedURLException e)
{
NucleusLogger.PLUGIN.warn(LOCALISER.msg("024010", urlStr), e);
return null;
}
}
else if (pluginURL.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 = pluginURL.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)
{
NucleusLogger.PLUGIN.warn(LOCALISER.msg("024010", urlStr), e);
return null;
}
}
else if (pluginURL.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 = pluginURL.toString().substring(12); //strip "code-source:"
urlStr = "jar:file:" + urlStr;
try
{
// URL for file containing the manifest
return new URL(urlStr);
}
catch (MalformedURLException e)
{
NucleusLogger.PLUGIN.warn(LOCALISER.msg("024010", urlStr), e);
return null;
}
} else if (pluginURL.toString().startsWith("bundle")) {
String urlString = pluginURL.toString();
urlString = urlString.substring(0, urlString.lastIndexOf("/"));
if (urlString.lastIndexOf("META-INF") >= 0)
urlString = urlString + "/MANIFEST.MF";
else
urlString = urlString + "/META-INF/MANIFEST.MF";
try {
logger.debug(urlString.toString());
return new URL(urlString);
} catch (MalformedURLException e) {
NucleusLogger.PLUGIN.warn(LOCALISER.msg("024010", urlString), e);
return null;
}
}
try
{
File file = new File(new URI(pluginURL.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)
{
NucleusLogger.PLUGIN.warn(LOCALISER.msg("024011", pluginURL), e);
return null;
}
}
}
}
catch (URISyntaxException use)
{
use.printStackTrace();
NucleusLogger.PLUGIN.warn(LOCALISER.msg("024011", pluginURL), use);
return null;
}
NucleusLogger.PLUGIN.warn(LOCALISER.msg("024012", pluginURL));
return null;
}
/**
* Loads a class (do not initialize) from an attribute of {@link org.datanucleus.plugin.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 java.io.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<Bundle> it = registeredPluginByPluginId.values().iterator();
while (it.hasNext())
{
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 = registeredPluginByPluginId.get(symbolicName);
if (requiredBundle == null) // TODO Add option of only logging problems in DataNucleus deps
{
if (bd.getParameter("resolution") != null &&
bd.getParameter("resolution").equalsIgnoreCase("optional"))
{
NucleusLogger.PLUGIN.debug(LOCALISER.msg("024013", bundle.getSymbolicName(), symbolicName));
}
else
{
NucleusLogger.PLUGIN.error(LOCALISER.msg("024014", bundle.getSymbolicName(), symbolicName));
}
}
if (bd.getParameter("bundle-version") != null)
{
if (requiredBundle != null &&
!isVersionInInterval(requiredBundle.getVersion(), bd.getParameter("bundle-version")))
{
NucleusLogger.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 registeredPluginByPluginId.values().toArray(new Bundle[registeredPluginByPluginId.values().size()]);
}
}