// ======================================================================== // Copyright (c) 2009-2010 Mortbay, Inc. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // You may elect to redistribute this code under either of these licenses. // Contributors: // Greg Wilkins - initial API and implementation // ======================================================================== package org.eclipse.jetty.osgi.boot; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.util.HashSet; import java.util.Iterator; import java.util.Map.Entry; import java.util.Set; import org.eclipse.jetty.deploy.App; import org.eclipse.jetty.deploy.AppProvider; import org.eclipse.jetty.deploy.DeploymentManager; import org.eclipse.jetty.deploy.providers.ContextProvider; import org.eclipse.jetty.deploy.providers.ScanningAppProvider; import org.eclipse.jetty.osgi.boot.utils.internal.PackageAdminServiceTracker; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.Scanner; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.webapp.WebAppContext; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleException; import org.osgi.framework.Constants; /** * AppProvider for OSGi. Supports the configuration of ContextHandlers and * WebApps. Extends the AbstractAppProvider to support the scanning of context * files located outside of the bundles. * <p> * This provider must not be called outside of jetty.boot: it should always be * called via the OSGi service listener. * </p> * <p> * This provider supports the same set of parameters than the WebAppProvider as * it supports the deployment of WebAppContexts. Except for the scanning of the * webapps directory. * </p> * <p> * When the parameter autoInstallOSGiBundles is set to true, OSGi bundles that * are located in the monitored directory are installed and started after the * framework as finished auto-starting all the other bundles. * Warning: only use this for development. * </p> */ public class OSGiAppProvider extends ScanningAppProvider implements AppProvider { private static final Logger LOG = Log.getLogger(OSGiAppProvider.class); private boolean _extractWars = true; private boolean _parentLoaderPriority = false; private String _defaultsDescriptor; private String _tldBundles; private String[] _configurationClasses; private boolean _autoInstallOSGiBundles = true; //Keep track of the bundles that were installed and that are waiting for the //framework to complete its initialization. Set<Bundle> _pendingBundlesToStart = null; /** * When a context file corresponds to a deployed bundle and is changed we * reload the corresponding bundle. */ private static class Filter implements FilenameFilter { OSGiAppProvider _enclosedInstance; public boolean accept(File dir, String name) { File file = new File(dir,name); if (fileMightBeAnOSGiBundle(file)) { return true; } if (!file.isDirectory()) { String contextName = getDeployedAppName(name); if (contextName != null) { App app = _enclosedInstance.getDeployedApps().get(contextName); return app != null; } } return false; } } /** * @param contextFileName * for example myContext.xml * @return The context, for example: myContext; null if this was not a * suitable contextFileName. */ private static String getDeployedAppName(String contextFileName) { String lowername = contextFileName.toLowerCase(); if (lowername.endsWith(".xml")) { String contextName = contextFileName.substring(0,lowername.length() - ".xml".length()); return contextName; } return null; } /** * Reading the display name of a webapp is really not sufficient for indexing the various * deployed ContextHandlers. * * @param context * @return */ private String getContextHandlerAppName(ContextHandler context) { String appName = context.getDisplayName(); if (appName == null || appName.length() == 0 || getDeployedApps().containsKey(appName)) { if (context instanceof WebAppContext) { appName = ((WebAppContext)context).getContextPath(); if (getDeployedApps().containsKey(appName)) { appName = "noDisplayName"+context.getClass().getSimpleName()+context.hashCode(); } } else { appName = "noDisplayName"+context.getClass().getSimpleName()+context.hashCode(); } } return appName; } /** * Default OSGiAppProvider consutructed when none are defined in the * jetty.xml configuration. */ public OSGiAppProvider() { super(new Filter()); ((Filter)super._filenameFilter)._enclosedInstance = this; } /** * Default OSGiAppProvider consutructed when none are defined in the * jetty.xml configuration. * * @param contextsDir */ public OSGiAppProvider(File contextsDir) throws IOException { this(); setMonitoredDirResource(Resource.newResource(contextsDir.toURI())); } /** * Returns the ContextHandler that was created by WebappRegistractionHelper * * @see AppProvider */ public ContextHandler createContextHandler(App app) throws Exception { // return pre-created Context ContextHandler wah = app.getContextHandler(); if (wah == null) { // for some reason it was not defined when the App was constructed. // we don't support this situation at this point. // once the WebAppRegistrationHelper is refactored, the code // that creates the ContextHandler will actually be here. throw new IllegalStateException("The App must be passed the " + "instance of the ContextHandler when it is construsted"); } if (_configurationClasses != null && wah instanceof WebAppContext) { ((WebAppContext)wah).setConfigurationClasses(_configurationClasses); } if (_defaultsDescriptor != null) ((WebAppContext)wah).setDefaultsDescriptor(_defaultsDescriptor); return app.getContextHandler(); } /** * @see AppProvider */ @Override public void setDeploymentManager(DeploymentManager deploymentManager) { // _manager=deploymentManager; super.setDeploymentManager(deploymentManager); } private static String getOriginId(Bundle contributor, String pathInBundle) { return contributor.getSymbolicName() + "-" + contributor.getVersion().toString() + (pathInBundle.startsWith("/") ? pathInBundle : "/" + pathInBundle); } /** * @param context * @throws Exception */ public void addContext(Bundle contributor, String pathInBundle, ContextHandler context) throws Exception { addContext(getOriginId(contributor, pathInBundle), context); } /** * @param context * @throws Exception */ public void addContext(String originId, ContextHandler context) throws Exception { // TODO apply configuration specific to this provider if (context instanceof WebAppContext) { ((WebAppContext)context).setExtractWAR(isExtract()); } // wrap context as an App App app = new App(getDeploymentManager(),this,originId,context); String appName = getContextHandlerAppName(context); getDeployedApps().put(appName,app); getDeploymentManager().addApp(app); } /** * Called by the scanner of the context files directory. If we find the * corresponding deployed App we reload it by returning the App. Otherwise * we return null and nothing happens: presumably the corresponding OSGi * webapp is not ready yet. * * @return the corresponding already deployed App so that it will be * reloaded. Otherwise returns null. */ @Override protected App createApp(String filename) { // find the corresponding bundle and ContextHandler or WebAppContext // and reload the corresponding App. // see the 2 pass of the refactoring of the WebAppRegistrationHelper. String name = getDeployedAppName(filename); if (name != null) { return getDeployedApps().get(name); } return null; } public void removeContext(ContextHandler context) throws Exception { String appName = getContextHandlerAppName(context); App app = getDeployedApps().remove(context.getDisplayName()); if (app == null) { //try harder to undeploy this context handler. //see bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=330098 appName = null; for (Entry<String,App> deployedApp : getDeployedApps().entrySet()) { if (deployedApp.getValue().getContextHandler() == context) { app = deployedApp.getValue(); appName = deployedApp.getKey(); break; } } if (appName != null) { getDeployedApps().remove(appName); } } if (app != null) { getDeploymentManager().removeApp(app); } } // //copied from WebAppProvider as the parameters are identical. // //only removed the parameer related to extractWars. /* ------------------------------------------------------------ */ /** * Get the parentLoaderPriority. * * @return the parentLoaderPriority */ public boolean isParentLoaderPriority() { return _parentLoaderPriority; } /* ------------------------------------------------------------ */ /** * Set the parentLoaderPriority. * * @param parentLoaderPriority * the parentLoaderPriority to set */ public void setParentLoaderPriority(boolean parentLoaderPriority) { _parentLoaderPriority = parentLoaderPriority; } /* ------------------------------------------------------------ */ /** * Get the defaultsDescriptor. * * @return the defaultsDescriptor */ public String getDefaultsDescriptor() { return _defaultsDescriptor; } /* ------------------------------------------------------------ */ /** * Set the defaultsDescriptor. * * @param defaultsDescriptor * the defaultsDescriptor to set */ public void setDefaultsDescriptor(String defaultsDescriptor) { _defaultsDescriptor = defaultsDescriptor; } /** * The context xml directory. In fact it is the directory watched by the * scanner. */ public File getContextXmlDirAsFile() { try { Resource monitoredDir = getMonitoredDirResource(); if (monitoredDir == null) return null; return monitoredDir.getFile(); } catch (IOException e) { e.printStackTrace(); return null; } } /* ------------------------------------------------------------ */ /** * The context xml directory. In fact it is the directory watched by the * scanner. */ public String getContextXmlDir() { try { Resource monitoredDir = getMonitoredDirResource(); if (monitoredDir == null) return null; return monitoredDir.getFile().toURI().toString(); } catch (IOException e) { e.printStackTrace(); return null; } } public boolean isExtract() { return _extractWars; } public void setExtract(boolean extract) { _extractWars=extract; } /** * @return true when this app provider locates osgi bundles and features in * its monitored directory and installs them. By default true if there is a folder to monitor. */ public boolean isAutoInstallOSGiBundles() { return _autoInstallOSGiBundles; } /** * <autoInstallOSGiBundles>true</autoInstallOSGiBundles> * @param installingOSGiBundles */ public void setAutoInstallOSGiBundles(boolean installingOSGiBundles) { _autoInstallOSGiBundles=installingOSGiBundles; } /* ------------------------------------------------------------ */ /** * Set the directory in which to look for context XML files. * <p> * If a webapp call "foo/" or "foo.war" is discovered in the monitored * directory, then the ContextXmlDir is examined to see if a foo.xml file * exists. If it does, then this deployer will not deploy the webapp and the * ContextProvider should be used to act on the foo.xml file. * </p> * <p> * Also if this directory contains some osgi bundles, it will install them. * </p> * * @see ContextProvider * @param contextsDir */ public void setContextXmlDir(String contextsDir) { setMonitoredDirName(contextsDir); } /** * @param tldBundles Comma separated list of bundles that contain tld jars * that should be setup on the jetty instances created here. */ public void setTldBundles(String tldBundles) { _tldBundles = tldBundles; } /** * @return The list of bundles that contain tld jars that should be setup * on the jetty instances created here. */ public String getTldBundles() { return _tldBundles; } /** * @param configurations The configuration class names. */ public void setConfigurationClasses(String[] configurations) { _configurationClasses = configurations==null?null:(String[])configurations.clone(); } /* ------------------------------------------------------------ */ /** * */ public String[] getConfigurationClasses() { return _configurationClasses; } /** * Overridden to install the OSGi bundles found in the monitored folder. */ @Override protected void doStart() throws Exception { if (isAutoInstallOSGiBundles()) { if (getMonitoredDirResource() == null) { setAutoInstallOSGiBundles(false); LOG.info("Disable autoInstallOSGiBundles as there is not contexts folder to monitor."); } else { File scandir = null; try { scandir = getMonitoredDirResource().getFile(); if (!scandir.exists() || !scandir.isDirectory()) { setAutoInstallOSGiBundles(false); LOG.warn("Disable autoInstallOSGiBundles as the contexts folder '" + scandir.getAbsolutePath() + " does not exist."); scandir = null; } } catch (IOException ioe) { setAutoInstallOSGiBundles(false); LOG.warn("Disable autoInstallOSGiBundles as the contexts folder '" + getMonitoredDirResource().getURI() + " does not exist."); scandir = null; } if (scandir != null) { for (File file : scandir.listFiles()) { if (fileMightBeAnOSGiBundle(file)) { installBundle(file, false); } } } } } super.doStart(); if (isAutoInstallOSGiBundles()) { Scanner.ScanCycleListener scanCycleListner = new AutoStartWhenFrameworkHasCompleted(this); super.addScannerListener(scanCycleListner); } } /** * When the file is a jar or a folder, we look if it looks like an OSGi bundle. * In that case we install it and start it. * <p> * Really a simple trick to get going quickly with development. * </p> */ @Override protected void fileAdded(String filename) throws Exception { File file = new File(filename); if (isAutoInstallOSGiBundles() && file.exists() && fileMightBeAnOSGiBundle(file)) { installBundle(file, true); } else { super.fileAdded(filename); } } /** * @param file * @return */ private static boolean fileMightBeAnOSGiBundle(File file) { if (file.isDirectory()) { if (new File(file,"META-INF/MANIFEST.MF").exists()) { return true; } } else if (file.getName().endsWith(".jar")) { return true; } return false; } @Override protected void fileChanged(String filename) throws Exception { File file = new File(filename); if (isAutoInstallOSGiBundles() && fileMightBeAnOSGiBundle(file)) { updateBundle(file); } else { super.fileChanged(filename); } } @Override protected void fileRemoved(String filename) throws Exception { File file = new File(filename); if (isAutoInstallOSGiBundles() && fileMightBeAnOSGiBundle(file)) { uninstallBundle(file); } else { super.fileRemoved(filename); } } /** * Returns a bundle according to its location. * In the version 1.6 of org.osgi.framework, BundleContext.getBundle(String) is what we want. * However to support older versions of OSGi. We use our own local refrence mechanism. * @param location * @return */ protected Bundle getBundle(BundleContext bc, String location) { //not available in older versions of OSGi: //return bc.getBundle(location); for (Bundle b : bc.getBundles()) { if (b.getLocation().equals(location)) { return b; } } return null; } protected synchronized Bundle installBundle(File file, boolean start) { try { BundleContext bc = JettyBootstrapActivator.getBundleContext(); String location = file.toURI().toString(); Bundle b = getBundle(bc, location); if (b == null) { b = bc.installBundle(location); } if (b == null) { //not sure we will ever be here, //most likely a BundleException was thrown LOG.warn("The file " + location + " is not an OSGi bundle."); return null; } if (start && b.getHeaders().get(Constants.FRAGMENT_HOST) == null) {//not a fragment, try to start it. if the framework has finished auto-starting. if (!PackageAdminServiceTracker.INSTANCE.frameworkHasCompletedAutostarts()) { if (_pendingBundlesToStart == null) { _pendingBundlesToStart = new HashSet<Bundle>(); } _pendingBundlesToStart.add(b); return null; } else { b.start(); } } return b; } catch (BundleException e) { LOG.warn("Unable to " + (start? "start":"install") + " the bundle " + file.getAbsolutePath(), e); } return null; } protected void uninstallBundle(File file) { try { Bundle b = getBundle(JettyBootstrapActivator.getBundleContext(), file.toURI().toString()); b.stop(); b.uninstall(); } catch (BundleException e) { LOG.warn("Unable to uninstall the bundle " + file.getAbsolutePath(), e); } } protected void updateBundle(File file) { try { Bundle b = getBundle(JettyBootstrapActivator.getBundleContext(), file.toURI().toString()); if (b == null) { installBundle(file, true); } else if (b.getState() == Bundle.ACTIVE) { b.update(); } else { b.start(); } } catch (BundleException e) { LOG.warn("Unable to update the bundle " + file.getAbsolutePath(), e); } } } /** * At the end of each scan, if there are some bundles to be started, * look if the framework has completed its autostart. In that case start those bundles. */ class AutoStartWhenFrameworkHasCompleted implements Scanner.ScanCycleListener { private static final Logger LOG = Log.getLogger(AutoStartWhenFrameworkHasCompleted.class); private final OSGiAppProvider _appProvider; AutoStartWhenFrameworkHasCompleted(OSGiAppProvider appProvider) { _appProvider = appProvider; } public void scanStarted(int cycle) throws Exception { } public void scanEnded(int cycle) throws Exception { if (_appProvider._pendingBundlesToStart != null && PackageAdminServiceTracker.INSTANCE.frameworkHasCompletedAutostarts()) { Iterator<Bundle> it = _appProvider._pendingBundlesToStart.iterator(); while (it.hasNext()) { Bundle b = it.next(); if (b.getHeaders().get(Constants.FRAGMENT_HOST) != null) { continue; } try { b.start(); } catch (BundleException e) { LOG.warn("Unable to start the bundle " + b.getLocation(), e); } } _appProvider._pendingBundlesToStart = null; } } }