All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.jangaroo.apprunner.util.JettyWrapper Maven / Gradle / Ivy

There is a newer version: 4.1.17
Show newest version
package net.jangaroo.apprunner.util;

import net.jangaroo.apprunner.proxy.JangarooProxyServlet;
import org.apache.commons.lang3.Range;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
import org.eclipse.jetty.webapp.WebAppContext;
import org.mitre.dsmiley.httpproxy.ProxyServlet;
import org.slf4j.Logger;

import javax.servlet.Servlet;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import static java.lang.invoke.MethodHandles.lookup;
import static java.util.Collections.addAll;
import static org.slf4j.LoggerFactory.getLogger;

/**
 * Wrapper class for controlling a Jetty server.
 */
public class JettyWrapper {

  private static final long WAIT_TIME_MILLIS = 1000L;
  /**
   * For the special port of zero, retry this given number of times
   * to get a new random port, in case other services already blocked
   * that port meanwhile.
   */
  private static final int RANDOM_PORT_RETRY_LIMIT = 20;
  private static final Integer RANDOM_PORT_IDENTIFIER = 0;

  public static final String ROOT_PATH = "/";

  public static final String ROOT_PATH_SPEC = "/*";
  private static final StaticResourcesServletConfig DEFAULT_RESOURCES_SERVLET_CONFIG =
          new StaticResourcesServletConfig(ROOT_PATH_SPEC, "/");

  static {
    Resource.setDefaultUseCaches(false);
  }

  private static final Logger LOG = getLogger(lookup().lookupClass());

  private final Map configurationByPath = new HashMap<>();

  private Server server;

  private Class webAppContextClass = WebAppContext.class;

  /**
   * Creates a Wrapper for controlling a Jetty server.
   *
   * @param baseDirs the base directories for serving static resources
   */
  public JettyWrapper(Path... baseDirs) {
    addBaseDirs(baseDirs);
  }

  public void addBaseDir(Path baseDir) {
    addBaseDir(baseDir, ROOT_PATH);
  }

  @SuppressWarnings("WeakerAccess")
  public void addBaseDirs(Path... baseDirs) {
    addAll(getConfiguration(ROOT_PATH).baseDirs, baseDirs);
  }

  public void setWebAppContextClass(Class webAppContextClass) {
    this.webAppContextClass = webAppContextClass;
  }

  public void addBaseDir(Path baseDir, String path) {
    getConfiguration(path).addBaseDir(baseDir);
  }

  public void addResourceJar(File resourceJar) {
    addResourceJar(resourceJar, ROOT_PATH);
  }

  public void addResourceJar(File resourceJar, String path) {
    addBaseDirInResourceJar(resourceJar, "META-INF/resources", path);
  }

  public void addBaseDirInResourceJar(File resourceJar, String relativePathInsideJar) {
    addBaseDirInResourceJar(resourceJar, relativePathInsideJar, ROOT_PATH);
  }

  public void addBaseDirInResourceJar(File resourceJar, String relativePathInsideJar, String path) {
    getConfiguration(path).addResourceJar(new JarFileWithRelativePath(resourceJar, relativePathInsideJar));
  }

  public void setStaticResourcesServletConfigs(List staticResourcesServletConfigs) {
    setStaticResourcesServletConfigs(staticResourcesServletConfigs, ROOT_PATH);
  }

  public void setStaticResourcesServletConfigs(List staticResourcesServletConfigs, String path) {
    getConfiguration(path).setStaticResourcesServletConfigs(staticResourcesServletConfigs);
  }

  public void setProxyServletConfigs(List proxyServletConfigs) {
    setProxyServletConfigs(proxyServletConfigs, ROOT_PATH);
  }

  public void setProxyServletConfigs(List proxyServletConfigs, String path) {
    getConfiguration(path).setProxyServletConfigs(proxyServletConfigs);
  }

  public void setAdditionalServlets(Map additionalServlets) {
    setAdditionalServlets(additionalServlets, ROOT_PATH);
  }

  public void setAdditionalServlets(Map additionalServlets, String path) {
    getConfiguration(path).setAdditionalServlets(additionalServlets);
  }

  public void start(String host, int port) throws JettyWrapperException {
    start(host, Range.is(port));
  }

  public void start(String host, Range portRange) throws JettyWrapperException {
    if (server != null) {
      stop();
    }

    Exception lastException = null;
    HandlerCollection handlerCollection = new HandlerCollection();
    ArrayList paths = new ArrayList<>(configurationByPath.keySet());
    // sort paths so that the deeper paths have precedence
    paths.sort((path1, path2) -> {
      String[] dirs1 = path1.split("/");
      String[] dirs2 = path2.split("/");
      return dirs2.length - dirs1.length;
    });
    for (String path : paths) {
      Configuration configuration = getConfiguration(path);
      WebAppContext handler = createHandler(
              path,
              configuration.baseDirs,
              configuration.jarFilesWithRelativePath,
              configuration.staticResourcesServletConfigs,
              configuration.proxyServletConfigs,
              configuration.additionalServlets
      );
      handlerCollection.addHandler(handler);
    }


    List shuffledPorts = shufflePortRange(portRange);
    for (Integer port : shuffledPorts) {
      try {
        tryServerStart(host, port, handlerCollection);
        break;
      } catch (Exception e) {
        getLog().debug("Could not start server", e);
        lastException = e;
      }
    }

    if (server == null || !server.isRunning()) {
      throw new JettyWrapperException("Could not start server", lastException);
    }
  }

  /**
   * Tries to start the server.
   *
   * @param host    host for server
   * @param port    port for server; zero will use a random port (and retry several times upon failure)
   * @param handler server handler
   * @throws Exception exception on failure; on retries, the last exception
   */
  private void tryServerStart(String host, Integer port, Handler handler) throws Exception {
    int retryLimit = RANDOM_PORT_IDENTIFIER.equals(port) ? RANDOM_PORT_RETRY_LIMIT : 1;
    Exception lastException = null;

    for (int i = 0; i < retryLimit; i++) {
      try {
        server = null;
        server = createServer(host, port, handler);
        server.start();
        break;
      } catch (Exception e) {
        lastException = e;
      }
    }

    if (lastException != null) {
      throw lastException;
    }
  }

  public void stop() {
    if (server != null) {
      try {
        server.stop();
      } catch (Exception e) {
        getLog().warn("Could not stop server", e);
      }
    }
  }

  public URI getUri() {
    return server.getURI().resolve(ROOT_PATH);
  }

  public void waitUntilStarted(int timeoutMillis) throws JettyWrapperException {
    if (server == null) {
      throw new JettyWrapperException("Server not started");
    }

    getLog().info(String.format("Waiting for Server startup (timeout = %d ms)", timeoutMillis));

    int waitedMillis = 0;

    try {
      while (waitedMillis < timeoutMillis && !server.isStarted()) {
        getLog().debug("Server not ready yet. Waiting ...");
        Thread.sleep(WAIT_TIME_MILLIS);
        waitedMillis += WAIT_TIME_MILLIS;
      }
    } catch (InterruptedException e) {
      getLog().warn("Interrupted while waiting for startup", e);
      Thread.currentThread().interrupt();
    }

    if (!server.isStarted()) {
      throw new JettyWrapperException(String.format("Server did not start within %d ms", timeoutMillis));
    }
  }

  public void blockUntilInterrupted() {
    try {
      server.join();
    } catch (InterruptedException ignore) {
      Thread.currentThread().interrupt();
    }
  }

  private Configuration getConfiguration(String path) {
    if (path.contains("\\")) {
      getLog().warn("Path should not contain backslashes: " + path);
    }
    if (!configurationByPath.containsKey(path)) {
      configurationByPath.put(path, new Configuration());
    }
    return configurationByPath.get(path);
  }

  private Server createServer(String host, int port, Handler handler) {
    InetSocketAddress address = new InetSocketAddress(host, port);
    getLog().info("Creating Jetty server at " + address);
    Server server = new Server(address);
    server.setHandler(handler);
    return server;
  }

  private WebAppContext createHandler(String path, List baseDirs, List resourceJarsWithRelativePaths, List staticResourcesServletConfigs, List proxyServletConfigs, Map additionalServlets) throws JettyWrapperException {
    // root path needs a root path servlet. make sure it is created.
    boolean hasRootPathServlet = !(ROOT_PATH.equals(path));
    try {
      getLog().info("Setting up handler with context path " + path);
      WebAppContext handler = webAppContextClass.newInstance();
      handler.setContextPath(path);

      handler.setInitParameter("org.eclipse.jetty.servlet.Default.useFileMappedBuffer", "false");

      List baseResources = baseDirs.stream().map(Resource::newResource).collect(Collectors.toList());
      baseResources = new ArrayList<>(baseResources);
      if (!resourceJarsWithRelativePaths.isEmpty()) {
        baseResources.addAll(
                resourceJarsWithRelativePaths.stream()
                        .map(JarFileWithRelativePath::toResource)
                        .filter(Objects::nonNull)
                        .collect(Collectors.toList()));
      }
      handler.setBaseResource(new ResourceCollection(baseResources.stream().filter(Resource::exists).toArray(Resource[]::new)));
      getLog().info("  Using base resources: " + baseResources);

      if (staticResourcesServletConfigs != null && !staticResourcesServletConfigs.isEmpty()) {
        for (StaticResourcesServletConfig config : staticResourcesServletConfigs) {
          hasRootPathServlet = addDefaultServlet(handler, config) || hasRootPathServlet;
        }
      }

      if (proxyServletConfigs != null && !proxyServletConfigs.isEmpty()) {
        for (ProxyServletConfig config : proxyServletConfigs) {
          hasRootPathServlet = addProxyServlet(handler, config) || hasRootPathServlet;
        }
      }

      if (additionalServlets != null && !additionalServlets.isEmpty()) {
        for (Map.Entry servletEntry : additionalServlets.entrySet()) {
          hasRootPathServlet = addServlet(handler, new ServletHolder(servletEntry.getValue()), servletEntry.getKey()) || hasRootPathServlet;
        }
      }

      if (!hasRootPathServlet) {
        addDefaultServlet(handler, DEFAULT_RESOURCES_SERVLET_CONFIG);
      }

      return handler;
    } catch (Exception e) {
      throw new JettyWrapperException(e);
    }
  }

  private boolean addDefaultServlet(ServletContextHandler webAppContext, StaticResourcesServletConfig config) {
    String pathSpec = config.getPathSpec();
    ServletHolder servletHolder = new ServletHolder(DefaultServlet.class);

    servletHolder.setInitParameter("relativeResourceBase", config.getRelativeResourceBase());
    servletHolder.setInitParameter("cacheControl", "no-store, no-cache, must-revalidate, max-age=0");

    webAppContext.addAliasCheck(new AllowSymLinkAliasChecker());
    boolean addedRootPathServlet = addServlet(webAppContext, servletHolder, pathSpec);
    getLog().info(String.format("  Serving static resources: %s -> %s",
            resolve(webAppContext.getContextPath(), pathSpec), config.getRelativeResourceBase()));
    return addedRootPathServlet;
  }

  private boolean addProxyServlet(ServletContextHandler webAppContext, ProxyServletConfig config) {
    String pathSpec = config.getPathSpec();
    ServletHolder servletHolder = new ServletHolder(pathSpec, JangarooProxyServlet.class); // TODO: Is this really a good servlet name?

    servletHolder.setInitParameter("targetUri", config.getTargetUri().replaceAll("/$", ""));
    servletHolder.setInitParameter(ProxyServlet.P_FORWARDEDFOR, String.valueOf(config.isForwardedHeaderEnabled()));
    servletHolder.setInitParameter(ProxyServlet.P_LOG, String.valueOf(config.isLoggingEnabled()));
    servletHolder.setInitParameter(ProxyServlet.P_PRESERVECOOKIES, "true");

    boolean addedRootPathServlet = addServlet(webAppContext, servletHolder, pathSpec);
    getLog().info(String.format("  Proxying requests: %s -> %s",
            resolve(webAppContext.getContextPath(), pathSpec), config.getTargetUri()));
    return addedRootPathServlet;
  }

  private boolean addServlet(ServletContextHandler webAppContext, ServletHolder servletHolder, String pathSpec) {
    webAppContext.addServlet(servletHolder, pathSpec);
    return ROOT_PATH_SPEC.equals(pathSpec);
  }

  private List shufflePortRange(Range portRange) {
    List shuffledPorts = new ArrayList<>();
    for (int i = portRange.getMinimum(); i <= portRange.getMaximum(); i++) {
      shuffledPorts.add(i);
    }
    Collections.shuffle(shuffledPorts);
    return shuffledPorts;
  }

  protected Logger getLog() {
    return LOG;
  }

  private static String resolve(String base, String relative) {
    return (ROOT_PATH.equals(base) ? ""  : base) + relative;
  }

  public static final class JettyWrapperException extends Exception {
    JettyWrapperException(String message) {
      super(message);
    }

    JettyWrapperException(String message, Throwable cause) {
      super(message, cause);
    }

    JettyWrapperException(Throwable cause) {
      super(cause);
    }
  }

  private static class JarFileWithRelativePath {
    public final File jarFile;
    public final String relativePath;

    private JarFileWithRelativePath(File jarFile, String relativePath) {
      this.jarFile = jarFile;
      this.relativePath = relativePath;
    }

    public Resource toResource() {
      try {
        return Resource.newResource("jar:" + Resource.toURL(jarFile).toString() + "!/" + relativePath);
      } catch (IOException e) {
        return null;
      }
    }
  }

  private static class Configuration {
    private final List baseDirs = new ArrayList<>();
    private final List jarFilesWithRelativePath = new ArrayList<>();
    private List staticResourcesServletConfigs;
    private List proxyServletConfigs;
    private Map additionalServlets;

    public void addBaseDir(Path baseDir) {
      baseDirs.add(baseDir);
    }

    public void addBaseDirs(Path...baseDirs) {
      addAll(this.baseDirs, baseDirs);
    }

    public void setStaticResourcesServletConfigs(List staticResourcesServletConfigs) {
      this.staticResourcesServletConfigs = staticResourcesServletConfigs;
    }

    public void addResourceJar(JarFileWithRelativePath jarFileWithRelativePath) {
      jarFilesWithRelativePath.add(jarFileWithRelativePath);
    }

    public void setProxyServletConfigs(List proxyServletConfigs) {
      this.proxyServletConfigs = proxyServletConfigs;
    }

    public void setAdditionalServlets(Map additionalServlets) {
      this.additionalServlets = additionalServlets;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy