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 }