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