1 // ======================================================================== 2 // Copyright (c) 2009-2010 Intalio, Inc. 3 // ------------------------------------------------------------------------ 4 // All rights reserved. This program and the accompanying materials 5 // are made available under the terms of the Eclipse Public License v1.0 6 // and Apache License v2.0 which accompanies this distribution. 7 // The Eclipse Public License is available at 8 // http://www.eclipse.org/legal/epl-v10.html 9 // The Apache License v2.0 is available at 10 // http://www.opensource.org/licenses/apache2.0.php 11 // You may elect to redistribute this code under either of these licenses. 12 // ======================================================================== 13 package org.eclipse.jetty.osgi.boot.internal.webapp; 14 15 import java.net.URL; 16 import java.util.Dictionary; 17 18 import org.eclipse.jetty.osgi.boot.JettyBootstrapActivator; 19 import org.eclipse.jetty.osgi.boot.OSGiWebappConstants; 20 import org.eclipse.jetty.util.log.Log; 21 import org.eclipse.jetty.util.log.Logger; 22 import org.osgi.framework.Bundle; 23 import org.osgi.framework.BundleEvent; 24 import org.osgi.util.tracker.BundleTracker; 25 import org.osgi.util.tracker.BundleTrackerCustomizer; 26 27 /** 28 * Support bundles that declare the webapp directly through headers in their 29 * manifest. 30 * <p> 31 * Those headers will define a new WebApplication: 32 * <ul> 33 * <li>Web-ContextPath</li> 34 * <li>Jetty-WarFolderPath</li> 35 * </ul> 36 * </p> 37 * <p> 38 * Those headers will define a new app started via a jetty-context or a list of 39 * them. ',' column is the separator between the various context files. 40 * <ul> 41 * <li>Jetty-ContextFilePath</li> 42 * </ul> 43 * </p> 44 * And generate a jetty WebAppContext or another ContextHandler then registers 45 * it as service. Kind of simpler than declarative services and their xml files. 46 * Also avoid having the contributing bundle depend on jetty's package for 47 * WebApp. 48 * 49 * @author hmalphettes 50 */ 51 public class WebBundleTrackerCustomizer implements BundleTrackerCustomizer 52 { 53 private static final Logger LOG = Log.getLogger(WebBundleTrackerCustomizer.class); 54 55 /** 56 * A bundle is being added to the <code>BundleTracker</code>. 57 * 58 * <p> 59 * This method is called before a bundle which matched the search parameters 60 * of the <code>BundleTracker</code> is added to the 61 * <code>BundleTracker</code>. This method should return the object to be 62 * tracked for the specified <code>Bundle</code>. The returned object is 63 * stored in the <code>BundleTracker</code> and is available from the 64 * {@link BundleTracker#getObject(Bundle) getObject} method. 65 * 66 * @param bundle The <code>Bundle</code> being added to the 67 * <code>BundleTracker</code>. 68 * @param event The bundle event which caused this customizer method to be 69 * called or <code>null</code> if there is no bundle event 70 * associated with the call to this method. 71 * @return The object to be tracked for the specified <code>Bundle</code> 72 * object or <code>null</code> if the specified <code>Bundle</code> 73 * object should not be tracked. 74 */ 75 public Object addingBundle(Bundle bundle, BundleEvent event) 76 { 77 if (bundle.getState() == Bundle.ACTIVE) 78 { 79 boolean isWebBundle = register(bundle); 80 return isWebBundle ? bundle : null; 81 } 82 else if (bundle.getState() == Bundle.STOPPING) 83 { 84 unregister(bundle); 85 } 86 else 87 { 88 // we should not be called in that state as 89 // we are registered only for ACTIVE and STOPPING 90 } 91 return null; 92 } 93 94 /** 95 * A bundle tracked by the <code>BundleTracker</code> has been modified. 96 * 97 * <p> 98 * This method is called when a bundle being tracked by the 99 * <code>BundleTracker</code> has had its state modified. 100 * 101 * @param bundle The <code>Bundle</code> whose state has been modified. 102 * @param event The bundle event which caused this customizer method to be 103 * called or <code>null</code> if there is no bundle event 104 * associated with the call to this method. 105 * @param object The tracked object for the specified bundle. 106 */ 107 public void modifiedBundle(Bundle bundle, BundleEvent event, Object object) 108 { 109 // nothing the web-bundle was already track. something changed. 110 // we only reload the webapps if the bundle is stopped and restarted. 111 if (bundle.getState() == Bundle.STOPPING || bundle.getState() == Bundle.ACTIVE) 112 { 113 unregister(bundle); 114 } 115 if (bundle.getState() == Bundle.ACTIVE) 116 { 117 register(bundle); 118 } 119 } 120 121 /** 122 * A bundle tracked by the <code>BundleTracker</code> has been removed. 123 * 124 * <p> 125 * This method is called after a bundle is no longer being tracked by the 126 * <code>BundleTracker</code>. 127 * 128 * @param bundle The <code>Bundle</code> that has been removed. 129 * @param event The bundle event which caused this customizer method to be 130 * called or <code>null</code> if there is no bundle event 131 * associated with the call to this method. 132 * @param object The tracked object for the specified bundle. 133 */ 134 public void removedBundle(Bundle bundle, BundleEvent event, Object object) 135 { 136 unregister(bundle); 137 } 138 139 /** 140 * @param bundle 141 * @return true if this bundle in indeed a web-bundle. 142 */ 143 private boolean register(Bundle bundle) 144 { 145 Dictionary<?, ?> dic = bundle.getHeaders(); 146 String warFolderRelativePath = (String) dic.get(OSGiWebappConstants.JETTY_WAR_FOLDER_PATH); 147 if (warFolderRelativePath != null) 148 { 149 String contextPath = getWebContextPath(bundle, dic, false); 150 if (contextPath == null || !contextPath.startsWith("/")) 151 { 152 LOG.warn("The manifest header '" + OSGiWebappConstants.JETTY_WAR_FOLDER_PATH 153 + ": " 154 + warFolderRelativePath 155 + "' in the bundle " 156 + bundle.getSymbolicName() 157 + " is not valid: there is no Web-ContextPath defined in the manifest."); 158 return false; 159 } 160 // create the corresponding service and publish it in the context of 161 // the contributor bundle. 162 try 163 { 164 JettyBootstrapActivator.registerWebapplication(bundle, warFolderRelativePath, contextPath); 165 return true; 166 } 167 catch (Throwable e) 168 { 169 LOG.warn("Starting the web-bundle " + bundle.getSymbolicName() + " threw an exception.", e); 170 return true;// maybe it did not work maybe it did. safer to track this bundle. 171 } 172 } 173 else if (dic.get(OSGiWebappConstants.JETTY_CONTEXT_FILE_PATH) != null) 174 { 175 String contextFileRelativePath = (String) dic.get(OSGiWebappConstants.JETTY_CONTEXT_FILE_PATH); 176 if (contextFileRelativePath == null) 177 { 178 // nothing to register here. 179 return false; 180 } 181 // support for multiple webapps in the same bundle: 182 String[] pathes = contextFileRelativePath.split(",;"); 183 for (String path : pathes) 184 { 185 try 186 { 187 JettyBootstrapActivator.registerContext(bundle, path.trim()); 188 } 189 catch (Throwable e) 190 { 191 LOG.warn(e); 192 } 193 } 194 return true; 195 } 196 else 197 { 198 // support for OSGi-RFC66; disclaimer, no access to the actual 199 // (draft) of the spec: just a couple of posts on the 200 // world-wide-web. 201 URL rfc66Webxml = bundle.getEntry("/WEB-INF/web.xml"); 202 if (rfc66Webxml == null && dic.get(OSGiWebappConstants.RFC66_WEB_CONTEXTPATH) == null) 203 { 204 return false;// no webapp in here 205 } 206 // this is risky: should we make sure that there is no classes and 207 // jars directly available 208 // at the root of the of the bundle: otherwise they are accessible 209 // through the browser. we should enforce that the whole classpath 210 // is 211 // pointing to files and folders inside WEB-INF. We should 212 // filter-out 213 // META-INF too 214 String rfc66ContextPath = getWebContextPath(bundle, dic, rfc66Webxml == null); 215 try 216 { 217 JettyBootstrapActivator.registerWebapplication(bundle, ".", rfc66ContextPath); 218 return true; 219 } 220 catch (Throwable e) 221 { 222 LOG.warn(e); 223 return true;// maybe it did not work maybe it did. safer to track this bundle. 224 } 225 } 226 } 227 228 private String getWebContextPath(Bundle bundle, Dictionary<?, ?> dic, boolean webinfWebxmlExists) 229 { 230 String rfc66ContextPath = (String) dic.get(OSGiWebappConstants.RFC66_WEB_CONTEXTPATH); 231 if (rfc66ContextPath == null) 232 { 233 if (!webinfWebxmlExists) { return null; } 234 // extract from the last token of the bundle's location: 235 // (really ? 236 // could consider processing the symbolic name as an alternative 237 // the location will often reflect the version. 238 // maybe this is relevant when the file is a war) 239 String location = bundle.getLocation(); 240 String toks[] = location.replace('\\', '/').split("/"); 241 rfc66ContextPath = toks[toks.length - 1]; 242 // remove .jar, .war etc: 243 int lastDot = rfc66ContextPath.lastIndexOf('.'); 244 if (lastDot != -1) 245 { 246 rfc66ContextPath = rfc66ContextPath.substring(0, lastDot); 247 } 248 } 249 if (!rfc66ContextPath.startsWith("/")) 250 { 251 rfc66ContextPath = "/" + rfc66ContextPath; 252 } 253 return rfc66ContextPath; 254 } 255 256 private void unregister(Bundle bundle) 257 { 258 // nothing to do: when the bundle is stopped, each one of its service 259 // reference is also stopped and that is what we use to stop the 260 // corresponding 261 // webapps registered in that bundle. 262 } 263 264 }