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 }