View Javadoc

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 }