View Javadoc

1   /*
2    * Copyright 2006-2008 the original author or authors.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.osgi.web.deployer.jetty;
18  
19  import java.io.File;
20  import java.io.IOException;
21  
22  import org.mortbay.jetty.Server;
23  import org.mortbay.jetty.handler.ContextHandlerCollection;
24  import org.mortbay.jetty.handler.HandlerCollection;
25  import org.mortbay.jetty.webapp.WebAppContext;
26  import org.mortbay.resource.Resource;
27  import org.mortbay.util.IO;
28  import org.osgi.framework.Bundle;
29  import org.springframework.osgi.util.OsgiBundleUtils;
30  import org.springframework.osgi.util.OsgiStringUtils;
31  import org.springframework.osgi.web.deployer.OsgiWarDeploymentException;
32  import org.springframework.osgi.web.deployer.WarDeployment;
33  import org.springframework.osgi.web.deployer.WarDeploymentContext;
34  import org.springframework.osgi.web.deployer.internal.util.Utils;
35  import org.springframework.osgi.web.deployer.support.AbstractWarDeployer;
36  import org.springframework.util.Assert;
37  
38  /**
39   * <a href="http://jetty.mortbay.org">Jetty</a> 6.1.x+ specific war deployer.
40   * Unpacks the given bundle into a temporary folder which is then used for
41   * deploying the war into the web container. While the bundle could be used in
42   * packed formed by Jetty, for performance reasons and JSP support (through
43   * Jasper) an unpack, file-system based format is required (thus the unpacking).
44   * 
45   * <p/>The deployer works with a {@link Server} instance which can be either
46   * configured by the user ({@link #setServer(Object)} or detected automatically
47   * by the deployer.
48   * 
49   * @see WebAppContext
50   * 
51   * @author Costin Leau
52   * 
53   */
54  public class JettyWarDeployer extends AbstractWarDeployer {
55  
56  	/** Jetty system classes */
57  	// these are loaded by the war parent class-loader
58  	private static final String[] systemClasses = { "java.", "javax.servlet.", "javax.xml.", "org.mortbay." };
59  
60  	/** Jetty server classes */
61  	// these aren't loaded by the war class-loader
62  	private static final String[] serverClasses = { "-org.mortbay.jetty.plus.jaas.", "org.mortbay.jetty." };
63  
64  	/** access to Jetty server service */
65  	private Server serverService;
66  
67  
68  	/**
69  	 * Sets the Jetty Server used by this deployer. If none is set (the
70  	 * default), the deployer will look for an OSGi service, matching the
71  	 * {@link Server} interface (using a timeout of 5 seconds).
72  	 * 
73  	 * <p/> To avoid the dependencies on Jetty classes in its signature, this
74  	 * setter accepts a plain Object that is checked and casted internally.
75  	 * 
76  	 * @param server Jetty server (normally a Spring-DM OSGi service reference)
77  	 */
78  	public void setServer(Object server) {
79  		if (server != null) {
80  			Assert.isInstanceOf(Server.class, server, "Invalid Jetty Server given:");
81  			this.serverService = (Server) server;
82  		}
83  	}
84  
85  	public void afterPropertiesSet() throws Exception {
86  		super.afterPropertiesSet();
87  
88  		if (serverService == null) {
89  			log.info("No Jetty Server set; looking for one in the OSGi service registry...");
90  			try {
91  				serverService = (Server) Utils.createServerServiceProxy(getBundleContext(), Server.class, null);
92  				log.info("Found service " + serverService);
93  			}
94  			catch (RuntimeException ex) {
95  				log.error("No Jetty Server found, bailing out", ex);
96  				throw ex;
97  			}
98  		}
99  	}
100 
101 	/**
102 	 * {@inheritDoc}
103 	 * 
104 	 * Creates an OSGi-specific Jetty war deployer.
105 	 */
106 	protected WarDeployment createDeployment(Bundle bundle, String contextPath) throws Exception {
107 		WebAppContext wac = createJettyWebContext(bundle, contextPath);
108 		// FIXME: remove this once things are improved in Jetty (OSGI-438)
109 		wac.setAttribute(WarDeploymentContext.OSGI_BUNDLE_CONTEXT_ATTRIBUTE, OsgiBundleUtils.getBundleContext(bundle));
110 		JettyWarDeployment deployment = new JettyWarDeployment(new JettyContextUndeployer() {
111 
112 			public void undeploy(WebAppContext webAppCtx) throws OsgiWarDeploymentException {
113 				stopWebAppContext(webAppCtx);
114 			}
115 		}, bundle, wac);
116 
117 		return deployment;
118 	}
119 
120 	protected void startDeployment(WarDeployment deployment) throws Exception {
121 		Assert.isInstanceOf(JettyWarDeployment.class, deployment, "Wrong type of deployment used");
122 		startWebAppContext(((JettyWarDeployment) deployment).getWebAppContext());
123 	}
124 
125 	/**
126 	 * Creates the Jetty specific web context for the given OSGi bundle.
127 	 * 
128 	 * @param bundle
129 	 * @return
130 	 * @throws Exception
131 	 */
132 	private WebAppContext createJettyWebContext(Bundle bundle, String contextPath) throws Exception {
133 
134 		WebAppContext wac = new WebAppContext();
135 
136 		// create a jetty web app context
137 
138 		// the server is being used to generate the temp folder (so we have to set it)
139 		wac.setServer(serverService);
140 		// set the war string since it's used to generate the temp path
141 		wac.setWar(OsgiStringUtils.nullSafeName(bundle));
142 		// same goes for the context path (add leading "/" -> w/o the context will not work)
143 		wac.setContextPath(contextPath);
144 		// no hot deployment (at least not through directly Jetty)
145 		wac.setCopyWebDir(false);
146 		wac.setExtractWAR(true);
147 
148 		//
149 		// 1. resource settings
150 		//
151 
152 		// start with the slow, IO activity
153 		Resource rootResource = getRootResource(bundle, wac);
154 
155 		// wac needs access to the WAR root
156 		// we have to make sure we don't trigger any direct file lookup
157 		// so instead of calling .setWar()
158 		// we set the base resource directly
159 		wac.setBaseResource(rootResource);
160 		// reset the war setting (so that the base resource is used)
161 		wac.setWar(null);
162 
163 		// 
164 		// 2. class-loading behaviour
165 		//
166 
167 		// obey the servlet spec class-loading contract
168 		wac.setSystemClasses(systemClasses);
169 		wac.setServerClasses(serverClasses);
170 
171 		// no java 2 loading compliance
172 		wac.setParentLoaderPriority(false);
173 		// create special classloader
174 		wac.setClassLoader(Utils.createWebAppClassLoader(bundle, Server.class));
175 
176 		return wac;
177 	}
178 
179 	private Resource getRootResource(Bundle bundle, WebAppContext wac) throws Exception {
180 		// decide whether we unpack or not
181 		// unpack the war somewhere
182 		File unpackFolder = unpackBundle(bundle, wac);
183 
184 		return Resource.newResource(unpackFolder.getCanonicalPath());
185 
186 		//		else {
187 		//			((OsgiWebAppContext) wac).setBundle(bundle);
188 		//			// if it's unpacked, use the bundle API directly
189 		//			return new BundleSpaceJettyResource(bundle, "/");
190 		//		}
191 	}
192 
193 	/**
194 	 * Starts the Jetty web context class.
195 	 * 
196 	 * @param wac,
197 	 * @throws Exception
198 	 */
199 	private void startWebAppContext(WebAppContext wac) throws Exception {
200 		HandlerCollection contexts = getJettyContexts();
201 
202 		// set the TCCL since it's used internally by Jetty
203 		Thread current = Thread.currentThread();
204 		ClassLoader old = current.getContextClassLoader();
205 		try {
206 			current.setContextClassLoader(wac.getClassLoader());
207 			if (contexts != null) {
208 				contexts.addHandler(wac);
209 			}
210 			wac.start();
211 			if (contexts != null) {
212 				contexts.start();
213 			}
214 		}
215 		finally {
216 			current.setContextClassLoader(old);
217 		}
218 	}
219 
220 	/**
221 	 * Stops the given context.
222 	 * 
223 	 * @param wac
224 	 * @throws Exception
225 	 */
226 	private void stopWebAppContext(WebAppContext wac) throws OsgiWarDeploymentException {
227 
228 		Resource rootResource = wac.getBaseResource();
229 		String contextPath = wac.getContextPath();
230 
231 		String messageEnding = "context [" + contextPath + "] from server " + getServerInfo();
232 
233 		log.info("About to undeploy " + messageEnding);
234 
235 		HandlerCollection contexts = getJettyContexts();
236 
237 		Thread current = Thread.currentThread();
238 		ClassLoader old = current.getContextClassLoader();
239 		try {
240 			current.setContextClassLoader(wac.getClassLoader());
241 			wac.stop();
242 			if (contexts != null) {
243 				contexts.removeHandler(wac);
244 			}
245 			log.info("Context [" + contextPath + "] undeployed successfully from server " + getServerInfo());
246 		}
247 		catch (Exception ex) {
248 			throw new OsgiWarDeploymentException("Cannot undeploy " + messageEnding, ex);
249 		}
250 		finally {
251 			current.setContextClassLoader(old);
252 
253 			// clean up unpacked folder
254 			if (log.isDebugEnabled())
255 				log.debug("Cleaning unpacked folder " + rootResource);
256 			try {
257 				IO.delete(rootResource.getFile());
258 			}
259 			catch (IOException ex) {
260 				// it's clean up so there is nothing else we can do
261 				// log the error but ignore it otherwise
262 				log.warn("Could not clean unpacked folder for " + messageEnding, ex);
263 			}
264 		}
265 	}
266 
267 	private HandlerCollection getJettyContexts() {
268 		HandlerCollection contexts = (HandlerCollection) serverService.getChildHandlerByClass(ContextHandlerCollection.class);
269 		if (contexts == null)
270 			contexts = (HandlerCollection) serverService.getChildHandlerByClass(HandlerCollection.class);
271 
272 		return contexts;
273 	}
274 
275 	private File unpackBundle(Bundle bundle, WebAppContext wac) throws Exception {
276 		// Could use Jetty temporary folder
277 		// File extractedWebAppDir = new File(wac.getTempDirectory(), "webapp");
278 
279 		File tmpFile = File.createTempFile("jetty-" + wac.getContextPath().substring(1), ".osgi");
280 		tmpFile.delete();
281 		tmpFile.mkdir();
282 
283 		if (log.isDebugEnabled())
284 			log.debug("Unpacking bundle " + OsgiStringUtils.nullSafeNameAndSymName(bundle) + " to folder ["
285 					+ tmpFile.getCanonicalPath() + "] ...");
286 		Utils.unpackBundle(bundle, tmpFile);
287 
288 		return tmpFile;
289 	}
290 
291 	protected String getServerInfo() {
292 		return "Jetty-" + Server.getVersion();
293 	}
294 }