// ======================================================================== // Copyright (c) 2009-2010 Intalio, 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. // ======================================================================== package org.eclipse.jetty.osgi.boot.internal.webapp; import java.net.URL; import java.util.Dictionary; import org.eclipse.jetty.osgi.boot.JettyBootstrapActivator; import org.eclipse.jetty.osgi.boot.OSGiWebappConstants; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.osgi.framework.Bundle; import org.osgi.framework.BundleEvent; import org.osgi.util.tracker.BundleTracker; import org.osgi.util.tracker.BundleTrackerCustomizer; /** * Support bundles that declare the webapp directly through headers in their * manifest. * <p> * Those headers will define a new WebApplication: * <ul> * <li>Web-ContextPath</li> * <li>Jetty-WarFolderPath</li> * </ul> * </p> * <p> * Those headers will define a new app started via a jetty-context or a list of * them. ',' column is the separator between the various context files. * <ul> * <li>Jetty-ContextFilePath</li> * </ul> * </p> * And generate a jetty WebAppContext or another ContextHandler then registers * it as service. Kind of simpler than declarative services and their xml files. * Also avoid having the contributing bundle depend on jetty's package for * WebApp. * * @author hmalphettes */ public class WebBundleTrackerCustomizer implements BundleTrackerCustomizer { private static final Logger LOG = Log.getLogger(WebBundleTrackerCustomizer.class); /** * A bundle is being added to the <code>BundleTracker</code>. * * <p> * This method is called before a bundle which matched the search parameters * of the <code>BundleTracker</code> is added to the * <code>BundleTracker</code>. This method should return the object to be * tracked for the specified <code>Bundle</code>. The returned object is * stored in the <code>BundleTracker</code> and is available from the * {@link BundleTracker#getObject(Bundle) getObject} method. * * @param bundle The <code>Bundle</code> being added to the * <code>BundleTracker</code>. * @param event The bundle event which caused this customizer method to be * called or <code>null</code> if there is no bundle event associated * with the call to this method. * @return The object to be tracked for the specified <code>Bundle</code> * object or <code>null</code> if the specified <code>Bundle</code> * object should not be tracked. */ public Object addingBundle(Bundle bundle, BundleEvent event) { if (bundle.getState() == Bundle.ACTIVE) { boolean isWebBundle = register(bundle); return isWebBundle ? bundle : null; } else if (bundle.getState() == Bundle.STOPPING) { unregister(bundle); } else { //we should not be called in that state as //we are registered only for ACTIVE and STOPPING } return null; } /** * A bundle tracked by the <code>BundleTracker</code> has been modified. * * <p> * This method is called when a bundle being tracked by the * <code>BundleTracker</code> has had its state modified. * * @param bundle The <code>Bundle</code> whose state has been modified. * @param event The bundle event which caused this customizer method to be * called or <code>null</code> if there is no bundle event associated * with the call to this method. * @param object The tracked object for the specified bundle. */ public void modifiedBundle(Bundle bundle, BundleEvent event, Object object) { //nothing the web-bundle was already track. something changed. //we only reload the webapps if the bundle is stopped and restarted. // System.err.println(bundle.getSymbolicName()); if (bundle.getState() == Bundle.STOPPING || bundle.getState() == Bundle.ACTIVE) { unregister(bundle); } if (bundle.getState() == Bundle.ACTIVE) { register(bundle); } } /** * A bundle tracked by the <code>BundleTracker</code> has been removed. * * <p> * This method is called after a bundle is no longer being tracked by the * <code>BundleTracker</code>. * * @param bundle The <code>Bundle</code> that has been removed. * @param event The bundle event which caused this customizer method to be * called or <code>null</code> if there is no bundle event associated * with the call to this method. * @param object The tracked object for the specified bundle. */ public void removedBundle(Bundle bundle, BundleEvent event, Object object) { unregister(bundle); } /** * @param bundle * @return true if this bundle in indeed a web-bundle. */ private boolean register(Bundle bundle) { Dictionary<?, ?> dic = bundle.getHeaders(); String warFolderRelativePath = (String)dic.get(OSGiWebappConstants.JETTY_WAR_FOLDER_PATH); if (warFolderRelativePath != null) { String contextPath = getWebContextPath(bundle, dic, false);//(String)dic.get(OSGiWebappConstants.RFC66_WEB_CONTEXTPATH); if (contextPath == null || !contextPath.startsWith("/")) { LOG.warn("The manifest header '" + OSGiWebappConstants.JETTY_WAR_FOLDER_PATH + ": " + warFolderRelativePath + "' in the bundle " + bundle.getSymbolicName() + " is not valid: there is no Web-ContextPath defined in the manifest."); return false; } // create the corresponding service and publish it in the context of // the contributor bundle. try { JettyBootstrapActivator.registerWebapplication(bundle,warFolderRelativePath,contextPath); return true; } catch (Throwable e) { LOG.warn("Starting the web-bundle " + bundle.getSymbolicName() + " threw an exception.", e); return true;//maybe it did not work maybe it did. safer to track this bundle. } } else if (dic.get(OSGiWebappConstants.JETTY_CONTEXT_FILE_PATH) != null) { String contextFileRelativePath = (String)dic.get(OSGiWebappConstants.JETTY_CONTEXT_FILE_PATH); if (contextFileRelativePath == null) { // nothing to register here. return false; } // support for multiple webapps in the same bundle: String[] pathes = contextFileRelativePath.split(",;"); for (String path : pathes) { try { JettyBootstrapActivator.registerContext(bundle,path.trim()); } catch (Throwable e) { // TODO Auto-generated catch block e.printStackTrace(); } } return true; } else { // support for OSGi-RFC66; disclaimer, no access to the actual // (draft) of the spec: just a couple of posts on the // world-wide-web. URL rfc66Webxml = bundle.getEntry("/WEB-INF/web.xml"); if (rfc66Webxml == null && dic.get(OSGiWebappConstants.RFC66_WEB_CONTEXTPATH) == null) { return false;// no webapp in here } // this is risky: should we make sure that there is no classes and // jars directly available // at the root of the of the bundle: otherwise they are accessible // through the browser. we should enforce that the whole classpath // is // pointing to files and folders inside WEB-INF. We should // filter-out // META-INF too String rfc66ContextPath = getWebContextPath(bundle,dic,rfc66Webxml==null); try { JettyBootstrapActivator.registerWebapplication(bundle,".",rfc66ContextPath); return true; } catch (Throwable e) { // TODO Auto-generated catch block e.printStackTrace(); return true;//maybe it did not work maybe it did. safer to track this bundle. } } } private String getWebContextPath(Bundle bundle, Dictionary<?, ?> dic, boolean webinfWebxmlExists) { String rfc66ContextPath = (String)dic.get(OSGiWebappConstants.RFC66_WEB_CONTEXTPATH); if (rfc66ContextPath == null) { if (!webinfWebxmlExists) { return null; } // extract from the last token of the bundle's location: // (really ? // could consider processing the symbolic name as an alternative // the location will often reflect the version. // maybe this is relevant when the file is a war) String location = bundle.getLocation(); String toks[] = location.replace('\\','/').split("/"); rfc66ContextPath = toks[toks.length - 1]; // remove .jar, .war etc: int lastDot = rfc66ContextPath.lastIndexOf('.'); if (lastDot != -1) { rfc66ContextPath = rfc66ContextPath.substring(0,lastDot); } } if (!rfc66ContextPath.startsWith("/")) { rfc66ContextPath = "/" + rfc66ContextPath; } return rfc66ContextPath; } private void unregister(Bundle bundle) { // nothing to do: when the bundle is stopped, each one of its service // reference is also stopped and that is what we use to stop the // corresponding // webapps registered in that bundle. } }