/*
 * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
 * (the "License"). You may not use this work except in compliance with the License, which is
 * available at www.apache.org/licenses/LICENSE-2.0
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied, as more fully set forth in the License.
 *
 * See the NOTICE file distributed with this work for information regarding copyright ownership.
 */

package alluxio.web;

import static alluxio.Constants.REST_API_PREFIX;

import alluxio.AlluxioURI;
import alluxio.conf.Configuration;
import alluxio.conf.PropertyKey;
import alluxio.metrics.MetricsSystem;
import alluxio.metrics.sink.MetricsServlet;
import alluxio.metrics.sink.PrometheusMetricsServlet;

import com.google.common.base.Preconditions;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.EnumSet;
import javax.annotation.concurrent.NotThreadSafe;
import javax.servlet.DispatcherType;

/**
 * Class that bootstraps and starts a web server.
 */
@NotThreadSafe
public abstract class WebServer {
  private static final Logger LOG = LoggerFactory.getLogger(WebServer.class);
  private static final String DISABLED_METHODS = "TRACE,OPTIONS";
  private static final String THREAD_DUMP_PATH = REST_API_PREFIX + "/common/thread_dump";
  private static final String JMX_PATH = "/metrics/jmx";

  private final Server mServer;
  private final String mServiceName;
  private final InetSocketAddress mAddress;
  private final ServerConnector mServerConnector;
  private final ConstraintSecurityHandler mSecurityHandler;
  protected final ServletContextHandler mServletContextHandler;
  private final MetricsServlet mMetricsServlet = new MetricsServlet(MetricsSystem.METRIC_REGISTRY);
  private final PrometheusMetricsServlet mPMetricsServlet = new PrometheusMetricsServlet(
      MetricsSystem.METRIC_REGISTRY);

  protected ServerConnector getServerConnector() {
    return mServerConnector;
  }

  /**
   * Creates a new instance of {@link WebServer}. It pairs URLs with servlets and sets the webapp
   * folder.
   *
   * @param serviceName name of the web service
   * @param address address of the server
   */
  public WebServer(String serviceName, InetSocketAddress address) {
    Preconditions.checkNotNull(serviceName, "Service name cannot be null");
    Preconditions.checkNotNull(address, "Server address cannot be null");

    mAddress = address;
    mServiceName = serviceName;

    QueuedThreadPool threadPool = new QueuedThreadPool();
    threadPool.setName(mServiceName.replace(" ", "-").toUpperCase());
    int webThreadCount = Configuration.getInt(PropertyKey.WEB_THREADS);

    // Jetty needs at least (1 + selectors + acceptors) threads.
    threadPool.setMinThreads(webThreadCount * 2 + 1);
    threadPool.setMaxThreads(webThreadCount * 2 + 100);

    mServer = new Server(threadPool);

    mServerConnector = new ServerConnector(mServer);
    mServerConnector.setPort(mAddress.getPort());
    mServerConnector.setHost(mAddress.getAddress().getHostAddress());
    mServerConnector.setReuseAddress(true);

    mServer.addConnector(mServerConnector);

    // Open the connector here so we can resolve the port if we are selecting a free port.
    try {
      mServerConnector.open();
    } catch (IOException e) {
      throw new RuntimeException(
          String.format("Failed to listen on address %s for web server %s", mAddress, mServiceName),
          e);
    }

    System.setProperty("org.apache.jasper.compiler.disablejsr199", "false");
    mServletContextHandler = new ServletContextHandler(ServletContextHandler.SECURITY
        | ServletContextHandler.NO_SESSIONS);
    mServletContextHandler.setContextPath(AlluxioURI.SEPARATOR);

    // Disable specified methods on REST services
    mSecurityHandler = (ConstraintSecurityHandler) mServletContextHandler.getSecurityHandler();
    for (String s : DISABLED_METHODS.split(",")) {
      disableMethod(s);
    }
    mServletContextHandler.addServlet(StacksServlet.class, THREAD_DUMP_PATH);
    mServletContextHandler.addServlet(JmxServlet.class, JMX_PATH);
    mServletContextHandler.addFilter(CORSFilter.class, "/*",
        EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE,
            DispatcherType.ASYNC, DispatcherType.ERROR));
    HandlerList handlers = new HandlerList();
    handlers.setHandlers(new Handler[] {mMetricsServlet.getHandler(), mPMetricsServlet.getHandler(),
        mServletContextHandler, new DefaultHandler()});
    mServer.setHandler(handlers);
  }

  /**
   * Adds a handler.
   *
   * @param handler the handler to add
   */
  public void addHandler(AbstractHandler handler) {
    HandlerList handlers = new HandlerList();
    handlers.addHandler(handler);
    for (Handler h : mServer.getHandlers()) {
      handlers.addHandler(h);
    }
    mServer.setHandler(handlers);
  }

  /**
   * @param handler to use
   */
  public void setHandler(AbstractHandler handler) {
    mServer.setHandler(handler);
  }

  /**
   * @param method to disable
   */
  private void disableMethod(String method) {
    Constraint constraint = new Constraint();
    constraint.setAuthenticate(true);
    constraint.setName("Disable " + method);
    ConstraintMapping disableMapping = new ConstraintMapping();
    disableMapping.setConstraint(constraint);
    disableMapping.setMethod(method.toUpperCase());
    disableMapping.setPathSpec("/");

    mSecurityHandler.addConstraintMapping(disableMapping);
  }

  /**
   * @return the underlying Jetty server
   */
  public Server getServer() {
    return mServer;
  }

  /**
   * Gets the actual bind hostname.
   *
   * @return the bind host
   */
  public String getBindHost() {
    String bindHost = mServerConnector.getHost();
    return bindHost == null ? "0.0.0.0" : bindHost;
  }

  /**
   * Gets the actual port that the web server is listening on.
   *
   * @return the local port
   */
  public int getLocalPort() {
    return mServerConnector.getLocalPort();
  }

  /**
   * Shuts down the web server.
   */
  public void stop() throws Exception {
    // close all connectors and release all binding ports
    for (Connector connector : mServer.getConnectors()) {
      connector.stop();
    }

    mServer.stop();
  }

  /**
   * Starts the web server.
   */
  public void start() {
    try {
      LOG.info("{} starting @ {}", mServiceName, mAddress);
      mServer.start();
      LOG.info("{} started @ {}", mServiceName, mAddress);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}
