View Javadoc

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