Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / development / JettyContainerService.java
blob6b1aef2dde861583208817e2cb3a1a6a4b5ad7c5
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;
15 import com.google.common.collect.ImmutableList;
16 import com.google.common.net.HttpHeaders;
18 import org.mortbay.jetty.Server;
19 import org.mortbay.jetty.handler.HandlerWrapper;
20 import org.mortbay.jetty.nio.SelectChannelConnector;
21 import org.mortbay.jetty.servlet.ServletHolder;
22 import org.mortbay.jetty.servlet.SessionHandler;
23 import org.mortbay.jetty.webapp.Configuration;
24 import org.mortbay.jetty.webapp.JettyWebXmlConfiguration;
25 import org.mortbay.jetty.webapp.WebAppContext;
26 import org.mortbay.resource.Resource;
27 import org.mortbay.util.Scanner;
29 import java.io.File;
30 import java.io.FilenameFilter;
31 import java.io.IOException;
32 import java.net.URL;
33 import java.security.Permissions;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.Date;
37 import java.util.List;
38 import java.util.concurrent.Semaphore;
39 import java.util.logging.Level;
40 import java.util.logging.Logger;
42 import javax.servlet.RequestDispatcher;
43 import javax.servlet.ServletException;
44 import javax.servlet.http.HttpServlet;
45 import javax.servlet.http.HttpServletRequest;
46 import javax.servlet.http.HttpServletResponse;
47 import javax.servlet.http.HttpServletResponseWrapper;
49 /**
50 * Implements a Jetty backed {@link ContainerService}.
53 @ServiceProvider(ContainerService.class)
54 public class JettyContainerService extends AbstractContainerService {
56 private static final Logger log = Logger.getLogger(JettyContainerService.class.getName());
58 private static final String FILES_API_DEPRECATION_WARNING =
59 "The Google Cloud Storage Java API is deprecated and will soon be removed. Please use the"
60 + " Google Cloud Storage Client library instead. Migration documentation is available here:"
61 + " https://cloud.google.com/appengine/docs/java/googlecloudstorageclient/migrate";
63 public final static String WEB_DEFAULTS_XML =
64 "com/google/appengine/tools/development/webdefault.xml";
66 private static final int MAX_SIMULTANEOUS_API_CALLS = 100;
68 private static final Long SOFT_DEADLINE_DELAY_MS = 60000L;
70 /**
71 * Specify which {@link Configuration} objects should be invoked when
72 * configuring a web application.
74 * <p>This is a subset of:
75 * org.mortbay.jetty.webapp.WebAppContext.__dftConfigurationClasses
77 * <p>Specifically, we've removed {@link JettyWebXmlConfiguration} which
78 * allows users to use {@code jetty-web.xml} files.
80 private static final String CONFIG_CLASSES[] = new String[] {
81 "org.mortbay.jetty.webapp.WebXmlConfiguration",
82 "org.mortbay.jetty.webapp.TagLibConfiguration"
85 private static final String WEB_XML_ATTR =
86 "com.google.appengine.tools.development.webXml";
87 private static final String APPENGINE_WEB_XML_ATTR =
88 "com.google.appengine.tools.development.appEngineWebXml";
90 private boolean disableFilesApiWarning = false;
92 static {
93 System.setProperty("org.mortbay.log.class", JettyLogger.class.getName());
96 private final static int SCAN_INTERVAL_SECONDS = 5;
98 /**
99 * Jetty webapp context.
101 private WebAppContext context;
104 * Our webapp context.
106 private AppContext appContext;
109 * The Jetty server.
111 private Server server;
114 * Hot deployment support.
116 private Scanner scanner;
118 private class JettyAppContext implements AppContext {
119 @Override
120 public IsolatedAppClassLoader getClassLoader() {
121 return (IsolatedAppClassLoader) context.getClassLoader();
124 @Override
125 public Permissions getUserPermissions() {
126 return JettyContainerService.this.getUserPermissions();
129 @Override
130 public Permissions getApplicationPermissions() {
131 return getClassLoader().getAppPermissions();
134 @Override
135 public Object getContainerContext() {
136 return context;
140 public JettyContainerService() {
143 @Override
144 protected File initContext() throws IOException {
145 this.context = new DevAppEngineWebAppContext(appDir, externalResourceDir, devAppServerVersion,
146 apiProxyDelegate, devAppServer);
147 this.appContext = new JettyAppContext();
149 context.setDescriptor(webXmlLocation == null ? null : webXmlLocation.getAbsolutePath());
151 String webDefaultXml = devAppServer.getServiceProperties().get("appengine.webdefault.xml");
152 if (webDefaultXml == null) {
153 webDefaultXml = WEB_DEFAULTS_XML;
155 context.setDefaultsDescriptor(webDefaultXml);
157 context.setConfigurationClasses(CONFIG_CLASSES);
159 File appRoot = determineAppRoot();
160 installLocalInitializationEnvironment();
162 URL[] classPath = getClassPathForApp(appRoot);
163 context.setClassLoader(new IsolatedAppClassLoader(appRoot, externalResourceDir, classPath,
164 JettyContainerService.class.getClassLoader()));
165 if (Boolean.parseBoolean(System.getProperty("appengine.allowRemoteShutdown"))) {
166 context.addServlet(new ServletHolder(new ServerShutdownServlet()), "/_ah/admin/quit");
169 if (Boolean.parseBoolean(System.getProperty("appengine.disableFilesApiWarning"))) {
170 disableFilesApiWarning = true;
173 return appRoot;
176 static class ServerShutdownServlet extends HttpServlet {
177 @Override
178 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
179 resp.getWriter().println("Shutting down local server.");
180 resp.flushBuffer();
181 DevAppServer server = (DevAppServer) getServletContext().getAttribute(
182 "com.google.appengine.devappserver.Server");
183 server.gracefulShutdown();
187 @Override
188 protected void connectContainer() throws Exception {
189 moduleConfigurationHandle.checkEnvironmentVariables();
191 Thread currentThread = Thread.currentThread();
192 ClassLoader previousCcl = currentThread.getContextClassLoader();
193 currentThread.setContextClassLoader(null);
195 try {
196 SelectChannelConnector connector = new SelectChannelConnector();
197 connector.setHost(address);
198 connector.setPort(port);
199 connector.setSoLingerTime(0);
200 connector.open();
202 server = new Server();
203 server.addConnector(connector);
205 port = connector.getLocalPort();
206 } finally {
207 currentThread.setContextClassLoader(previousCcl);
211 @Override
212 protected void startContainer() throws Exception {
213 context.setAttribute(WEB_XML_ATTR, webXml);
214 context.setAttribute(APPENGINE_WEB_XML_ATTR, appEngineWebXml);
216 Thread currentThread = Thread.currentThread();
217 ClassLoader previousCcl = currentThread.getContextClassLoader();
218 currentThread.setContextClassLoader(null);
220 try {
221 ApiProxyHandler apiHandler = new ApiProxyHandler(appEngineWebXml);
222 apiHandler.setHandler(context);
224 server.setHandler(apiHandler);
225 SessionHandler handler = context.getSessionHandler();
226 if (isSessionsEnabled()) {
227 handler.setSessionManager(new SerializableObjectsOnlyHashSessionManager());
228 } else {
229 handler.setSessionManager(new StubSessionManager());
231 server.start();
232 } finally {
233 currentThread.setContextClassLoader(previousCcl);
237 @Override
238 protected void stopContainer() throws Exception {
239 server.stop();
243 * If the property "appengine.fullscan.seconds" is set to a positive integer, the web app
244 * content (deployment descriptors, classes/ and lib/) is scanned for changes that will trigger
245 * the reloading of the application.
246 * If the property is not set (default), we monitor the webapp war file or the
247 * appengine-web.xml in case of a pre-exploded webapp directory, and reload the webapp whenever an
248 * update is detected, i.e. a newer timestamp for the monitored file. As a single-context
249 * deployment, add/delete is not applicable here.
251 * appengine-web.xml will be reloaded too. However, changes that require a module instance
252 * restart, e.g. address/port, will not be part of the reload.
254 @Override
255 protected void startHotDeployScanner() throws Exception {
256 String fullScanInterval = System.getProperty("appengine.fullscan.seconds");
257 if (fullScanInterval != null) {
258 try {
259 int interval = Integer.parseInt(fullScanInterval);
260 if (interval < 1) {
261 log.info("Full scan of the web app for changes is disabled.");
262 return;
264 log.info("Full scan of the web app in place every " + interval + "s.");
265 fullWebAppScanner(interval);
266 return;
267 } catch (NumberFormatException ex) {
268 log.log(Level.WARNING, "appengine.fullscan.seconds property is not an integer:", ex);
269 log.log(Level.WARNING, "Using the default scanning method.");
272 scanner = new Scanner();
273 scanner.setScanInterval(SCAN_INTERVAL_SECONDS);
274 scanner.setScanDirs(ImmutableList.of(getScanTarget()));
275 scanner.setFilenameFilter(new FilenameFilter() {
276 @Override
277 public boolean accept(File dir, String name) {
278 try {
279 if (name.equals(getScanTarget().getName())) {
280 return true;
282 return false;
284 catch (Exception e) {
285 return false;
289 scanner.scan();
290 scanner.addListener(new ScannerListener());
291 scanner.start();
294 @Override
295 protected void stopHotDeployScanner() throws Exception {
296 if (scanner != null) {
297 scanner.stop();
299 scanner = null;
302 private class ScannerListener implements Scanner.DiscreteListener {
303 @Override
304 public void fileAdded(String filename) throws Exception {
305 fileChanged(filename);
308 @Override
309 public void fileChanged(String filename) throws Exception {
310 log.info(filename + " updated, reloading the webapp!");
311 reloadWebApp();
314 @Override
315 public void fileRemoved(String filename) throws Exception {
320 * To minimize the overhead, we point the scanner right to the single file in question.
322 private File getScanTarget() throws Exception {
323 if (appDir.isFile() || context.getWebInf() == null) {
324 return appDir;
325 } else {
326 return new File(context.getWebInf().getFile().getPath()
327 + File.separator + "appengine-web.xml");
331 private void fullWebAppScanner(int interval) throws IOException {
332 String webInf = context.getWebInf().getFile().getPath();
333 List<File> scanList = new ArrayList<File>();
334 Collections.addAll(scanList,
335 new File(webInf, "classes"),
336 new File(webInf, "lib"),
337 new File(webInf, "web.xml"),
338 new File(webInf, "appengine-web.xml"));
340 scanner = new Scanner();
341 scanner.setScanInterval(interval);
342 scanner.setScanDirs(scanList);
343 scanner.setReportExistingFilesOnStartup(false);
344 scanner.setRecursive(true);
345 scanner.scan();
347 scanner.addListener(new Scanner.BulkListener() {
348 @Override
349 public void filesChanged(List changedFiles) throws Exception {
350 log.info("A file has changed, reloading the web application.");
351 reloadWebApp();
355 scanner.start();
359 * Assuming Jetty handles race condition nicely, as this is how Jetty handles a hot deploy too.
361 @Override
362 protected void reloadWebApp() throws Exception {
363 server.getHandler().stop();
364 moduleConfigurationHandle.restoreSystemProperties();
365 moduleConfigurationHandle.readConfiguration();
366 moduleConfigurationHandle.checkEnvironmentVariables();
367 extractFieldsFromWebModule(moduleConfigurationHandle.getModule());
369 /** same as what's in startContainer, we need suppress the ContextClassLoader here. */
370 Thread currentThread = Thread.currentThread();
371 ClassLoader previousCcl = currentThread.getContextClassLoader();
372 currentThread.setContextClassLoader(null);
373 try {
374 initContext();
375 installLocalInitializationEnvironment();
377 if (!isSessionsEnabled()) {
378 context.getSessionHandler().setSessionManager(new StubSessionManager());
380 context.setAttribute(WEB_XML_ATTR, webXml);
381 context.setAttribute(APPENGINE_WEB_XML_ATTR, appEngineWebXml);
383 ApiProxyHandler apiHandler = new ApiProxyHandler(appEngineWebXml);
384 apiHandler.setHandler(context);
385 server.setHandler(apiHandler);
387 apiHandler.start();
388 } finally {
389 currentThread.setContextClassLoader(previousCcl);
393 @Override
394 public AppContext getAppContext() {
395 return appContext;
398 @Override
399 public void forwardToServer(HttpServletRequest hrequest,
400 HttpServletResponse hresponse) throws IOException, ServletException {
401 log.finest("forwarding request to module: " + appEngineWebXml.getModule() + "." + instance);
402 RequestDispatcher requestDispatcher =
403 context.getServletContext().getRequestDispatcher(hrequest.getRequestURI());
404 requestDispatcher.forward(hrequest, hresponse);
407 private File determineAppRoot() throws IOException {
408 Resource webInf = context.getWebInf();
409 if (webInf == null) {
410 if (userCodeClasspathManager.requiresWebInf()) {
411 throw new AppEngineConfigException(
412 "Supplied application has to contain WEB-INF directory.");
414 return appDir;
416 return webInf.getFile().getParentFile();
420 * {@code ApiProxyHandler} wraps around an existing {@link org.mortbay.jetty.Handler}
421 * and surrounds each top-level request (i.e. not includes or
422 * forwards) with a try finally block that maintains the {@link
423 * com.google.apphosting.api.ApiProxy.Environment} {@link ThreadLocal}.
425 private class ApiProxyHandler extends HandlerWrapper {
426 @SuppressWarnings("hiding")
427 private final AppEngineWebXml appEngineWebXml;
429 ApiProxyHandler(AppEngineWebXml appEngineWebXml) {
430 this.appEngineWebXml = appEngineWebXml;
433 @SuppressWarnings("unchecked")
434 @Override
435 public void handle(String target,
436 HttpServletRequest request,
437 HttpServletResponse response,
438 int dispatch) throws IOException, ServletException {
439 if (dispatch == REQUEST) {
440 long startTimeUsec = System.currentTimeMillis() * 1000;
441 Semaphore semaphore = new Semaphore(MAX_SIMULTANEOUS_API_CALLS);
443 LocalEnvironment env = new LocalHttpRequestEnvironment(appEngineWebXml.getAppId(),
444 WebModule.getModuleName(appEngineWebXml), appEngineWebXml.getMajorVersionId(),
445 instance, getPort(), request, SOFT_DEADLINE_DELAY_MS, modulesFilterHelper);
446 env.getAttributes().put(LocalEnvironment.API_CALL_SEMAPHORE, semaphore);
447 env.getAttributes().put(DEFAULT_VERSION_HOSTNAME, "localhost:" + devAppServer.getPort());
448 env.getAttributes().put(LocalEnvironment.FILESAPI_WAS_USED, false);
450 ApiProxy.setEnvironmentForCurrentThread(env);
452 RecordingResponseWrapper wrappedResponse = new RecordingResponseWrapper(response);
453 try {
454 super.handle(target, request, wrappedResponse, dispatch);
455 if (request.getRequestURI().startsWith(_AH_URL_RELOAD)) {
456 try {
457 reloadWebApp();
458 log.info("Reloaded the webapp context: " + request.getParameter("info"));
459 } catch (Exception ex) {
460 log.log(Level.WARNING, "Failed to reload the current webapp context.", ex);
463 } finally {
464 try {
465 semaphore.acquire(MAX_SIMULTANEOUS_API_CALLS);
466 } catch (InterruptedException ex) {
467 log.log(Level.WARNING, "Interrupted while waiting for API calls to complete:", ex);
469 env.callRequestEndListeners();
471 if (apiProxyDelegate instanceof ApiProxyLocal) {
472 ApiProxyLocal apiProxyLocal = (ApiProxyLocal) apiProxyDelegate;
473 try {
474 String appId = env.getAppId();
475 String versionId = env.getVersionId();
476 String requestId = DevLogHandler.getRequestId();
477 long endTimeUsec = new Date().getTime() * 1000;
479 LocalLogService logService = (LocalLogService)
480 apiProxyLocal.getService(LocalLogService.PACKAGE);
482 if (!disableFilesApiWarning
483 && (Boolean) env.getAttributes().get(LocalEnvironment.FILESAPI_WAS_USED)) {
484 log.warning(FILES_API_DEPRECATION_WARNING);
487 logService.addRequestInfo(appId, versionId, requestId,
488 request.getRemoteAddr(), request.getRemoteUser(),
489 startTimeUsec, endTimeUsec, request.getMethod(),
490 request.getRequestURI(), request.getProtocol(),
491 request.getHeader("User-Agent"), true,
492 wrappedResponse.getStatus(), request.getHeader(HttpHeaders.REFERER));
493 logService.clearResponseSize();
494 } finally {
495 ApiProxy.clearEnvironmentForCurrentThread();
499 } else {
500 super.handle(target, request, response, dispatch);
505 private class RecordingResponseWrapper extends HttpServletResponseWrapper {
506 private int status = SC_OK;
508 RecordingResponseWrapper(HttpServletResponse response) {
509 super(response);
512 @Override
513 public void setStatus(int sc) {
514 status = sc;
515 super.setStatus(sc);
518 public int getStatus() {
519 return status;
522 @Override
523 public void sendError(int sc) throws IOException {
524 status = sc;
525 super.sendError(sc);
528 @Override
529 public void sendError(int sc, String msg) throws IOException {
530 status = sc;
531 super.sendError(sc, msg);
534 @Override
535 public void sendRedirect(String location) throws IOException {
536 status = SC_MOVED_TEMPORARILY;
537 super.sendRedirect(location);
540 @Override
541 public void setStatus(int status, String string) {
542 super.setStatus(status, string);
543 this.status = status;
546 @Override
547 public void reset() {
548 super.reset();
549 this.status = SC_OK;