Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / development / JettyContainerService.java
blob1c31f873dc91fdcd1c4b13e65c30755dbba92770
1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.development;
5 import static com.google.appengine.tools.development.LocalEnvironment.DEFAULT_VERSION_HOSTNAME;
7 import com.google.appengine.api.log.dev.DevLogHandler;
8 import com.google.appengine.api.log.dev.LocalLogService;
9 import com.google.apphosting.api.ApiProxy;
10 import com.google.apphosting.utils.config.AppEngineConfigException;
11 import com.google.apphosting.utils.config.AppEngineWebXml;
12 import com.google.apphosting.utils.config.WebModule;
13 import com.google.apphosting.utils.jetty.JettyLogger;
14 import com.google.apphosting.utils.jetty.StubSessionManager;
16 import org.mortbay.jetty.Server;
17 import org.mortbay.jetty.handler.HandlerWrapper;
18 import org.mortbay.jetty.nio.SelectChannelConnector;
19 import org.mortbay.jetty.servlet.SessionHandler;
20 import org.mortbay.jetty.webapp.Configuration;
21 import org.mortbay.jetty.webapp.JettyWebXmlConfiguration;
22 import org.mortbay.jetty.webapp.WebAppContext;
23 import org.mortbay.resource.Resource;
24 import org.mortbay.util.Scanner;
26 import java.io.File;
27 import java.io.FilenameFilter;
28 import java.io.IOException;
29 import java.net.URL;
30 import java.security.Permissions;
31 import java.util.Date;
32 import java.util.concurrent.Semaphore;
33 import java.util.logging.Level;
34 import java.util.logging.Logger;
36 import javax.servlet.ServletException;
37 import javax.servlet.http.HttpServletRequest;
38 import javax.servlet.http.HttpServletResponse;
39 import javax.servlet.http.HttpServletResponseWrapper;
41 /**
42 * Implements a Jetty backed {@link ContainerService}.
45 @ServiceProvider(ContainerService.class)
46 public class JettyContainerService extends AbstractContainerService {
48 private static final Logger log = Logger.getLogger(JettyContainerService.class.getName());
50 public final static String WEB_DEFAULTS_XML =
51 "com/google/appengine/tools/development/webdefault.xml";
53 private static final int MAX_SIMULTANEOUS_API_CALLS = 100;
55 private static final Long SOFT_DEADLINE_DELAY_MS = 60000L;
57 /**
58 * Specify which {@link Configuration} objects should be invoked when
59 * configuring a web application.
61 * <p>This is a subset of:
62 * org.mortbay.jetty.webapp.WebAppContext.__dftConfigurationClasses
64 * <p>Specifically, we've removed {@link JettyWebXmlConfiguration} which
65 * allows users to use {@code jetty-web.xml} files.
67 private static final String CONFIG_CLASSES[] = new String[] {
68 "org.mortbay.jetty.webapp.WebXmlConfiguration",
69 "org.mortbay.jetty.webapp.TagLibConfiguration"
72 private static final String WEB_XML_ATTR =
73 "com.google.appengine.tools.development.webXml";
74 private static final String APPENGINE_WEB_XML_ATTR =
75 "com.google.appengine.tools.development.appEngineWebXml";
77 static {
78 System.setProperty("org.mortbay.log.class", JettyLogger.class.getName());
81 private final static int SCAN_INTERVAL_SECONDS = 5;
83 /**
84 * Jetty webapp context.
86 private WebAppContext context;
88 /**
89 * Our webapp context.
91 private AppContext appContext;
93 /**
94 * The Jetty server.
96 private Server server;
98 /**
99 * Hot deployment support.
101 private Scanner scanner;
103 private class JettyAppContext implements AppContext {
104 @Override
105 public IsolatedAppClassLoader getClassLoader() {
106 return (IsolatedAppClassLoader) context.getClassLoader();
109 @Override
110 public Permissions getUserPermissions() {
111 return JettyContainerService.this.getUserPermissions();
114 @Override
115 public Permissions getApplicationPermissions() {
116 return getClassLoader().getAppPermissions();
119 @Override
120 public Object getContainerContext() {
121 return context;
125 public JettyContainerService() {
128 @Override
129 protected File initContext() throws IOException {
130 this.context = new DevAppEngineWebAppContext(appDir, externalResourceDir, devAppServerVersion,
131 apiProxyLocal);
132 this.appContext = new JettyAppContext();
134 context.setDescriptor(webXmlLocation == null ? null : webXmlLocation.getAbsolutePath());
136 context.setDefaultsDescriptor(WEB_DEFAULTS_XML);
138 context.setConfigurationClasses(CONFIG_CLASSES);
140 File appRoot = determineAppRoot();
141 installLocalInitializationEnvironment();
143 URL[] classPath = getClassPathForApp(appRoot);
144 context.setClassLoader(new IsolatedAppClassLoader(appRoot, externalResourceDir, classPath,
145 JettyContainerService.class.getClassLoader()));
147 return appRoot;
150 @Override
151 protected void connectContainer() throws Exception {
152 serverConfigurationHandle.checkEnvironmentVariables();
154 Thread currentThread = Thread.currentThread();
155 ClassLoader previousCcl = currentThread.getContextClassLoader();
156 currentThread.setContextClassLoader(null);
158 try {
159 SelectChannelConnector connector = new SelectChannelConnector();
160 connector.setHost(address);
161 connector.setPort(port);
162 connector.setSoLingerTime(0);
163 connector.open();
165 server = new Server();
166 server.addConnector(connector);
168 port = connector.getLocalPort();
169 } finally {
170 currentThread.setContextClassLoader(previousCcl);
174 @Override
175 protected void startContainer() throws Exception {
176 context.setAttribute(WEB_XML_ATTR, webXml);
177 context.setAttribute(APPENGINE_WEB_XML_ATTR, appEngineWebXml);
179 Thread currentThread = Thread.currentThread();
180 ClassLoader previousCcl = currentThread.getContextClassLoader();
181 currentThread.setContextClassLoader(null);
183 try {
184 ApiProxyHandler apiHandler = new ApiProxyHandler(appEngineWebXml);
185 apiHandler.setHandler(context);
187 server.setHandler(apiHandler);
188 SessionHandler handler = context.getSessionHandler();
189 if (isSessionsEnabled()) {
190 handler.setSessionManager(new SerializableObjectsOnlyHashSessionManager());
191 } else {
192 handler.setSessionManager(new StubSessionManager());
194 server.start();
195 } finally {
196 currentThread.setContextClassLoader(previousCcl);
200 @Override
201 protected void stopContainer() throws Exception {
202 server.stop();
206 * Unlike the actual Jetty hot deployment support, we monitor the webapp war file or the
207 * appengine-web.xml in case of a pre-exploded webapp directory, and reload the webapp whenever an
208 * update is detected, i.e. a newer timestamp for the monitored file. As a single-context
209 * deployment, add/delete is not applicable here.
211 * appengine-web.xml will be reloaded too. However, changes that require a server restart, e.g.
212 * address/port, will not be part of the reload.
214 @Override
215 protected void startHotDeployScanner() throws Exception {
216 scanner = new Scanner();
217 scanner.setScanInterval(SCAN_INTERVAL_SECONDS);
218 scanner.setScanDir(getScanTarget());
219 scanner.setFilenameFilter(new FilenameFilter() {
220 @Override
221 public boolean accept(File dir, String name) {
222 try {
223 if (name.equals(getScanTarget().getName())) {
224 return true;
226 return false;
228 catch (Exception e) {
229 return false;
233 scanner.scan();
234 scanner.addListener(new ScannerListener());
235 scanner.start();
238 @Override
239 protected void stopHotDeployScanner() throws Exception {
240 if (scanner != null) {
241 scanner.stop();
243 scanner = null;
246 private class ScannerListener implements Scanner.DiscreteListener {
247 @Override
248 public void fileAdded(String filename) throws Exception {
249 fileChanged(filename);
252 @Override
253 public void fileChanged(String filename) throws Exception {
254 log.info(filename + " updated, reloading the webapp!");
255 reloadWebApp();
258 @Override
259 public void fileRemoved(String filename) throws Exception {
264 * To minimize the overhead, we point the scanner right to the single file in question.
266 private File getScanTarget() throws Exception {
267 if (appDir.isFile() || context.getWebInf() == null) {
268 return appDir;
269 } else {
270 return new File(context.getWebInf().getFile().getPath()
271 + File.separator + "appengine-web.xml");
276 * Assuming Jetty handles race condition nicely, as this is how Jetty handles a hot deploy too.
278 @Override
279 protected void reloadWebApp() throws Exception {
280 server.getHandler().stop();
281 serverConfigurationHandle.restoreSystemProperties();
282 serverConfigurationHandle.readConfiguration();
283 serverConfigurationHandle.checkEnvironmentVariables();
284 extractFieldsFromWebModule(serverConfigurationHandle.getModule());
286 /** same as what's in startContainer, we need suppress the ContextClassLoader here. */
287 Thread currentThread = Thread.currentThread();
288 ClassLoader previousCcl = currentThread.getContextClassLoader();
289 currentThread.setContextClassLoader(null);
290 try {
291 File webAppDir = initContext();
292 installLoggingServiceHandler();
293 installLocalInitializationEnvironment();
295 if (!isSessionsEnabled()) {
296 context.getSessionHandler().setSessionManager(new StubSessionManager());
298 context.setAttribute(WEB_XML_ATTR, webXml);
299 context.setAttribute(APPENGINE_WEB_XML_ATTR, appEngineWebXml);
301 ApiProxyHandler apiHandler = new ApiProxyHandler(appEngineWebXml);
302 apiHandler.setHandler(context);
303 server.setHandler(apiHandler);
305 apiHandler.start();
306 } finally {
307 currentThread.setContextClassLoader(previousCcl);
311 @Override
312 public AppContext getAppContext() {
313 return appContext;
316 private File determineAppRoot() throws IOException {
317 Resource webInf = context.getWebInf();
318 if (webInf == null) {
319 if (userCodeClasspathManager.requiresWebInf()) {
320 throw new AppEngineConfigException(
321 "Supplied application has to contain WEB-INF directory.");
323 return appDir;
325 return webInf.getFile().getParentFile();
329 * {@code ApiProxyHandler} wraps around an existing {@link org.mortbay.jetty.Handler}
330 * and surrounds each top-level request (i.e. not includes or
331 * forwards) with a try finally block that maintains the {@link
332 * com.google.apphosting.api.ApiProxy.Environment} {@link ThreadLocal}.
334 private class ApiProxyHandler extends HandlerWrapper {
335 private final AppEngineWebXml appEngineWebXml;
337 public ApiProxyHandler(AppEngineWebXml appEngineWebXml) {
338 this.appEngineWebXml = appEngineWebXml;
341 @SuppressWarnings("unchecked")
342 @Override
343 public void handle(String target,
344 HttpServletRequest request,
345 HttpServletResponse response,
346 int dispatch) throws IOException, ServletException {
347 if (dispatch == REQUEST) {
348 long startTimeUsec = System.currentTimeMillis() * 1000;
349 Semaphore semaphore = new Semaphore(MAX_SIMULTANEOUS_API_CALLS);
351 LocalEnvironment env = new LocalHttpRequestEnvironment(appEngineWebXml.getAppId(),
352 WebModule.getServerName(appEngineWebXml), appEngineWebXml.getMajorVersionId(),
353 instance, request, SOFT_DEADLINE_DELAY_MS);
354 env.getAttributes().put(LocalEnvironment.API_CALL_SEMAPHORE, semaphore);
355 env.getAttributes().put(DEFAULT_VERSION_HOSTNAME, "localhost:" + devAppServer.getPort());
357 ApiProxy.setEnvironmentForCurrentThread(env);
359 RecordingResponseWrapper wrappedResponse = new RecordingResponseWrapper(response);
360 try {
361 super.handle(target, request, wrappedResponse, dispatch);
362 if (request.getRequestURI().startsWith(_AH_URL_RELOAD)) {
363 try {
364 reloadWebApp();
365 log.info("Reloaded the webapp context: " + request.getParameter("info"));
366 } catch (Exception ex) {
367 log.log(Level.WARNING, "Failed to reload the current webapp context.", ex);
370 } finally {
371 try {
372 semaphore.acquire(MAX_SIMULTANEOUS_API_CALLS);
373 } catch (InterruptedException ex) {
374 log.log(Level.WARNING, "Interrupted while waiting for API calls to complete:", ex);
376 env.callRequestEndListeners();
378 try {
379 String appId = env.getAppId();
380 String versionId = env.getVersionId();
381 String requestId = DevLogHandler.getRequestId();
382 long endTimeUsec = new Date().getTime() * 1000;
384 LocalLogService logService = (LocalLogService)
385 apiProxyLocal.getService(LocalLogService.PACKAGE);
387 logService.addRequestInfo(appId, versionId, requestId,
388 request.getRemoteAddr(), request.getRemoteUser(),
389 startTimeUsec, endTimeUsec, request.getMethod(),
390 request.getRequestURI(), request.getProtocol(),
391 request.getHeader("User-Agent"), true,
392 wrappedResponse.getStatus(), request.getHeader("Referrer"));
393 logService.clearResponseSize();
394 } finally {
395 ApiProxy.clearEnvironmentForCurrentThread();
398 } else {
399 super.handle(target, request, response, dispatch);
404 private class RecordingResponseWrapper extends HttpServletResponseWrapper {
405 private int status = SC_OK;
407 RecordingResponseWrapper(HttpServletResponse response) {
408 super(response);
411 @Override
412 public void setStatus(int sc) {
413 status = sc;
414 super.setStatus(sc);
417 public int getStatus() {
418 return status;
421 @Override
422 public void sendError(int sc) throws IOException {
423 status = sc;
424 super.sendError(sc);
427 @Override
428 public void sendError(int sc, String msg) throws IOException {
429 status = sc;
430 super.sendError(sc, msg);
433 @Override
434 public void sendRedirect(String location) throws IOException {
435 status = SC_MOVED_TEMPORARILY;
436 super.sendRedirect(location);
439 @Override
440 public void setStatus(int status, String string) {
441 super.setStatus(status, string);
442 this.status = status;
445 @Override
446 public void reset() {
447 super.reset();
448 this.status = SC_OK;