Package com.google.appengine.tools.development

Source Code of com.google.appengine.tools.development.JettyContainerService$RecordingResponseWrapper

// Copyright 2008 Google Inc. All Rights Reserved.

package com.google.appengine.tools.development;

import static com.google.appengine.tools.development.LocalEnvironment.DEFAULT_VERSION_HOSTNAME;

import com.google.appengine.api.log.dev.DevLogHandler;
import com.google.appengine.api.log.dev.LocalLogService;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.utils.config.AppEngineConfigException;
import com.google.apphosting.utils.config.AppEngineWebXml;
import com.google.apphosting.utils.config.WebModule;
import com.google.apphosting.utils.jetty.JettyLogger;
import com.google.apphosting.utils.jetty.StubSessionManager;
import com.google.common.collect.ImmutableList;
import com.google.common.net.HttpHeaders;

import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.HandlerWrapper;
import org.mortbay.jetty.nio.SelectChannelConnector;
import org.mortbay.jetty.servlet.ServletHolder;
import org.mortbay.jetty.servlet.SessionHandler;
import org.mortbay.jetty.webapp.Configuration;
import org.mortbay.jetty.webapp.JettyWebXmlConfiguration;
import org.mortbay.jetty.webapp.WebAppContext;
import org.mortbay.resource.Resource;
import org.mortbay.util.Scanner;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URL;
import java.security.Permissions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

/**
* Implements a Jetty backed {@link ContainerService}.
*
*/
@ServiceProvider(ContainerService.class)
public class JettyContainerService extends AbstractContainerService {

  private static final Logger log = Logger.getLogger(JettyContainerService.class.getName());

  public final static String WEB_DEFAULTS_XML =
      "com/google/appengine/tools/development/webdefault.xml";

  private static final int MAX_SIMULTANEOUS_API_CALLS = 100;

  private static final Long SOFT_DEADLINE_DELAY_MS = 60000L;

  /**
   * Specify which {@link Configuration} objects should be invoked when
   * configuring a web application.
   *
   * <p>This is a subset of:
   *   org.mortbay.jetty.webapp.WebAppContext.__dftConfigurationClasses
   *
   * <p>Specifically, we've removed {@link JettyWebXmlConfiguration} which
   * allows users to use {@code jetty-web.xml} files.
   */
  private static final String CONFIG_CLASSES[] = new String[] {
        "org.mortbay.jetty.webapp.WebXmlConfiguration",
        "org.mortbay.jetty.webapp.TagLibConfiguration"
  };

  private static final String WEB_XML_ATTR =
      "com.google.appengine.tools.development.webXml";
  private static final String APPENGINE_WEB_XML_ATTR =
      "com.google.appengine.tools.development.appEngineWebXml";

  static {
    System.setProperty("org.mortbay.log.class", JettyLogger.class.getName());
  }

  private final static int SCAN_INTERVAL_SECONDS = 5;

  /**
   * Jetty webapp context.
   */
  private WebAppContext context;

  /**
   * Our webapp context.
   */
  private AppContext appContext;

  /**
   * The Jetty server.
   */
  private Server server;

  /**
   * Hot deployment support.
   */
  private Scanner scanner;

  private class JettyAppContext implements AppContext {
    @Override
    public IsolatedAppClassLoader getClassLoader() {
      return (IsolatedAppClassLoader) context.getClassLoader();
    }

    @Override
    public Permissions getUserPermissions() {
      return JettyContainerService.this.getUserPermissions();
    }

    @Override
    public Permissions getApplicationPermissions() {
      return getClassLoader().getAppPermissions();
    }

    @Override
    public Object getContainerContext() {
      return context;
    }
  }

  public JettyContainerService() {
  }

  @Override
  protected File initContext() throws IOException {
    this.context = new DevAppEngineWebAppContext(appDir, externalResourceDir, devAppServerVersion,
        apiProxyDelegate, devAppServer);
    this.appContext = new JettyAppContext();

    context.setDescriptor(webXmlLocation == null ? null : webXmlLocation.getAbsolutePath());

    String webDefaultXml = devAppServer.getServiceProperties().get("appengine.webdefault.xml");
    if (webDefaultXml == null) {
      webDefaultXml = WEB_DEFAULTS_XML;
    }
    context.setDefaultsDescriptor(webDefaultXml);

    context.setConfigurationClasses(CONFIG_CLASSES);

    File appRoot = determineAppRoot();
    installLocalInitializationEnvironment();

    URL[] classPath = getClassPathForApp(appRoot);
    context.setClassLoader(new IsolatedAppClassLoader(appRoot, externalResourceDir, classPath,
        JettyContainerService.class.getClassLoader()));
    if (Boolean.parseBoolean(System.getProperty("appengine.allowRemoteShutdown"))) {
      context.addServlet(new ServletHolder(new ServerShutdownServlet()), "/_ah/admin/quit");
    }

    return appRoot;
  }

  static class ServerShutdownServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
      resp.getWriter().println("Shutting down local server.");
      resp.flushBuffer();
      DevAppServer server = (DevAppServer) getServletContext().getAttribute(
          "com.google.appengine.devappserver.Server");
      server.gracefulShutdown();
    }
  }

  @Override
  protected void connectContainer() throws Exception {
    moduleConfigurationHandle.checkEnvironmentVariables();

    Thread currentThread = Thread.currentThread();
    ClassLoader previousCcl = currentThread.getContextClassLoader();
    currentThread.setContextClassLoader(null);

    try {
      SelectChannelConnector connector = new SelectChannelConnector();
      connector.setHost(address);
      connector.setPort(port);
      connector.setSoLingerTime(0);
      connector.open();

      server = new Server();
      server.addConnector(connector);

      port = connector.getLocalPort();
    } finally {
      currentThread.setContextClassLoader(previousCcl);
    }
  }

  @Override
  protected void startContainer() throws Exception {
    context.setAttribute(WEB_XML_ATTR, webXml);
    context.setAttribute(APPENGINE_WEB_XML_ATTR, appEngineWebXml);

    Thread currentThread = Thread.currentThread();
    ClassLoader previousCcl = currentThread.getContextClassLoader();
    currentThread.setContextClassLoader(null);

    try {
      ApiProxyHandler apiHandler = new ApiProxyHandler(appEngineWebXml);
      apiHandler.setHandler(context);

      server.setHandler(apiHandler);
      SessionHandler handler = context.getSessionHandler();
      if (isSessionsEnabled()) {
        handler.setSessionManager(new SerializableObjectsOnlyHashSessionManager());
      } else {
        handler.setSessionManager(new StubSessionManager());
      }
      server.start();
    } finally {
      currentThread.setContextClassLoader(previousCcl);
    }
  }

  @Override
  protected void stopContainer() throws Exception {
    server.stop();
  }

  /**
   *  If the property "appengine.fullscan.seconds" is set to a positive integer, the web app
   *  content (deployment descriptors, classes/ and lib/) is scanned for changes that will trigger
   *  the reloading of the application.
   *  If the property is not set (default), we monitor the webapp war file or the
   * appengine-web.xml in case of a pre-exploded webapp directory, and reload the webapp whenever an
   * update is detected, i.e. a newer timestamp for the monitored file. As a single-context
   * deployment, add/delete is not applicable here.
   *
   * appengine-web.xml will be reloaded too. However, changes that require a module instance
   * restart, e.g. address/port, will not be part of the reload.
   */
  @Override
  protected void startHotDeployScanner() throws Exception {
    String fullScanInterval = System.getProperty("appengine.fullscan.seconds");
    if (fullScanInterval != null) {
      try {
        int interval =  Integer.parseInt(fullScanInterval);
        if (interval < 1) {
          log.info("Full scan of the web app for changes is disabled.");
          return;
        }
        log.info("Full scan of the web app in place every " + interval + "s.");
        fullWebAppScanner(interval);
        return;
      } catch (NumberFormatException ex) {
        log.log(Level.WARNING, "appengine.fullscan.seconds property is not an integer:", ex);
        log.log(Level.WARNING, "Using the default scanning method.");
      }
    }
    scanner = new Scanner();
    scanner.setScanInterval(SCAN_INTERVAL_SECONDS);
    scanner.setScanDirs(ImmutableList.of(getScanTarget()));
    scanner.setFilenameFilter(new FilenameFilter() {
      @Override
      public boolean accept(File dir, String name) {
        try {
          if (name.equals(getScanTarget().getName())) {
            return true;
          }
          return false;
        }
        catch (Exception e) {
          return false;
        }
      }
    });
    scanner.scan();
    scanner.addListener(new ScannerListener());
    scanner.start();
  }

  @Override
  protected void stopHotDeployScanner() throws Exception {
    if (scanner != null) {
      scanner.stop();
    }
    scanner = null;
  }

  private class ScannerListener implements Scanner.DiscreteListener {
    @Override
    public void fileAdded(String filename) throws Exception {
      fileChanged(filename);
    }

    @Override
    public void fileChanged(String filename) throws Exception {
      log.info(filename + " updated, reloading the webapp!");
      reloadWebApp();
    }

    @Override
    public void fileRemoved(String filename) throws Exception {
    }
  }

  /**
   * To minimize the overhead, we point the scanner right to the single file in question.
   */
  private File getScanTarget() throws Exception {
    if (appDir.isFile() || context.getWebInf() == null) {
      return appDir;
    } else {
      return new File(context.getWebInf().getFile().getPath()
          + File.separator + "appengine-web.xml");
    }
  }

  private void fullWebAppScanner(int interval) throws IOException {
    String webInf = context.getWebInf().getFile().getPath();
    List<File> scanList = new ArrayList<File>();
    Collections.addAll(scanList,
        new File(webInf, "classes"),
        new File(webInf, "lib"),
        new File(webInf, "web.xml"),
        new File(webInf, "appengine-web.xml"));

    scanner = new Scanner();
    scanner.setScanInterval(interval);
    scanner.setScanDirs(scanList);
    scanner.setReportExistingFilesOnStartup(false);
    scanner.setRecursive(true);
    scanner.scan();

    scanner.addListener(new Scanner.BulkListener() {
      @Override
      public void filesChanged(List changedFiles) throws Exception {
        log.info("A file has changed, reloading the web application.");
        reloadWebApp();
      }
    });

    scanner.start();
  }

  /**
   * Assuming Jetty handles race condition nicely, as this is how Jetty handles a hot deploy too.
   */
  @Override
  protected void reloadWebApp() throws Exception {
    server.getHandler().stop();
    moduleConfigurationHandle.restoreSystemProperties();
    moduleConfigurationHandle.readConfiguration();
    moduleConfigurationHandle.checkEnvironmentVariables();
    extractFieldsFromWebModule(moduleConfigurationHandle.getModule());

    /** same as what's in startContainer, we need suppress the ContextClassLoader here. */
    Thread currentThread = Thread.currentThread();
    ClassLoader previousCcl = currentThread.getContextClassLoader();
    currentThread.setContextClassLoader(null);
    try {
      initContext();
      installLocalInitializationEnvironment();

      if (!isSessionsEnabled()) {
        context.getSessionHandler().setSessionManager(new StubSessionManager());
      }
      context.setAttribute(WEB_XML_ATTR, webXml);
      context.setAttribute(APPENGINE_WEB_XML_ATTR, appEngineWebXml);

      ApiProxyHandler apiHandler = new ApiProxyHandler(appEngineWebXml);
      apiHandler.setHandler(context);
      server.setHandler(apiHandler);

      apiHandler.start();
    } finally {
      currentThread.setContextClassLoader(previousCcl);
    }
  }

  @Override
  public AppContext getAppContext() {
    return appContext;
  }

  @Override
  public void forwardToServer(HttpServletRequest hrequest,
      HttpServletResponse hresponse) throws IOException, ServletException {
    log.finest("forwarding request to module: " + appEngineWebXml.getModule() + "." + instance);
    RequestDispatcher requestDispatcher =
        context.getServletContext().getRequestDispatcher(hrequest.getRequestURI());
    requestDispatcher.forward(hrequest, hresponse);
  }

  private File determineAppRoot() throws IOException {
    Resource webInf = context.getWebInf();
    if (webInf == null) {
      if (userCodeClasspathManager.requiresWebInf()) {
        throw new AppEngineConfigException(
            "Supplied application has to contain WEB-INF directory.");
      }
      return appDir;
    }
    return webInf.getFile().getParentFile();
  }

  /**
   * {@code ApiProxyHandler} wraps around an existing {@link  org.mortbay.jetty.Handler}
   * and surrounds each top-level request (i.e. not includes or
   * forwards) with a try finally block that maintains the {@link
   * com.google.apphosting.api.ApiProxy.Environment} {@link ThreadLocal}.
   */
  private class ApiProxyHandler extends HandlerWrapper {
    @SuppressWarnings("hiding")
    private final AppEngineWebXml appEngineWebXml;

    ApiProxyHandler(AppEngineWebXml appEngineWebXml) {
      this.appEngineWebXml = appEngineWebXml;
    }

    @SuppressWarnings("unchecked")
    @Override
    public void handle(String target,
                       HttpServletRequest request,
                       HttpServletResponse response,
                       int dispatch) throws IOException, ServletException {
      if (dispatch == REQUEST) {
        long startTimeUsec = System.currentTimeMillis() * 1000;
        Semaphore semaphore = new Semaphore(MAX_SIMULTANEOUS_API_CALLS);

        LocalEnvironment env = new LocalHttpRequestEnvironment(appEngineWebXml.getAppId(),
            WebModule.getModuleName(appEngineWebXml), appEngineWebXml.getMajorVersionId(),
            instance, getPort(), request, SOFT_DEADLINE_DELAY_MS, modulesFilterHelper);
        env.getAttributes().put(LocalEnvironment.API_CALL_SEMAPHORE, semaphore);
        env.getAttributes().put(DEFAULT_VERSION_HOSTNAME, "localhost:" + devAppServer.getPort());

        ApiProxy.setEnvironmentForCurrentThread(env);

        RecordingResponseWrapper wrappedResponse = new RecordingResponseWrapper(response);
        try {
          super.handle(target, request, wrappedResponse, dispatch);
          if (request.getRequestURI().startsWith(_AH_URL_RELOAD)) {
            try {
              reloadWebApp();
              log.info("Reloaded the webapp context: " + request.getParameter("info"));
            } catch (Exception ex) {
              log.log(Level.WARNING, "Failed to reload the current webapp context.", ex);
            }
          }
        } finally {
          try {
            semaphore.acquire(MAX_SIMULTANEOUS_API_CALLS);
          } catch (InterruptedException ex) {
            log.log(Level.WARNING, "Interrupted while waiting for API calls to complete:", ex);
          }
          env.callRequestEndListeners();

          if (apiProxyDelegate instanceof ApiProxyLocal) {
            ApiProxyLocal apiProxyLocal = (ApiProxyLocal) apiProxyDelegate;
            try {
              String appId = env.getAppId();
              String versionId = env.getVersionId();
              String requestId = DevLogHandler.getRequestId();
              long endTimeUsec = new Date().getTime() * 1000;

              LocalLogService logService = (LocalLogService)
                  apiProxyLocal.getService(LocalLogService.PACKAGE);

              logService.addRequestInfo(appId, versionId, requestId,
                  request.getRemoteAddr(), request.getRemoteUser(),
                  startTimeUsec, endTimeUsec, request.getMethod(),
                  request.getRequestURI(), request.getProtocol(),
                  request.getHeader("User-Agent"), true,
                  wrappedResponse.getStatus(), request.getHeader(HttpHeaders.REFERER));
              logService.clearResponseSize();
            } finally {
              ApiProxy.clearEnvironmentForCurrentThread();
            }
          }
        }
      } else {
        super.handle(target, request, response, dispatch);
      }
    }
  }

  private class RecordingResponseWrapper extends HttpServletResponseWrapper {
    private int status = SC_OK;

    RecordingResponseWrapper(HttpServletResponse response) {
      super(response);
    }

    @Override
    public void setStatus(int sc) {
      status = sc;
      super.setStatus(sc);
    }

    public int getStatus() {
      return status;
    }

    @Override
    public void sendError(int sc) throws IOException {
      status = sc;
      super.sendError(sc);
    }

    @Override
    public void sendError(int sc, String msg) throws IOException {
      status = sc;
      super.sendError(sc, msg);
    }

    @Override
    public void sendRedirect(String location) throws IOException {
        status = SC_MOVED_TEMPORARILY;
        super.sendRedirect(location);
    }

    @Override
    public void setStatus(int status, String string) {
        super.setStatus(status, string);
        this.status = status;
    }

    @Override
    public void reset() {
        super.reset();
        this.status = SC_OK;
    }
  }
}
TOP

Related Classes of com.google.appengine.tools.development.JettyContainerService$RecordingResponseWrapper

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.