
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.ambari.scom;


import com.google.gson.Gson;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Scopes;
import com.google.inject.Singleton;
import com.google.inject.persist.Transactional;
import com.google.inject.persist.jpa.JpaPersistModule;
import com.sun.jersey.spi.container.servlet.ServletContainer;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.api.AmbariPersistFilter;
import org.apache.ambari.server.configuration.ComponentSSLConfiguration;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.controller.HostsMap;
import org.apache.ambari.server.orm.GuiceJpaInitializer;
import org.apache.ambari.server.orm.PersistenceType;
import org.apache.ambari.server.security.authorization.AmbariLdapAuthenticationProvider;
import org.apache.ambari.server.security.authorization.AmbariLocalUserDetailsService;
import org.apache.ambari.server.security.authorization.Users;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.filter.DelegatingFilterProxy;

import javax.crypto.BadPaddingException;
import java.io.File;
import java.net.BindException;
import java.util.Map;
import java.util.Properties;

/**
 * Main Ambari server class.
 */
@Singleton
public class AmbariServer {
  /**
   * The Jetty server.
   */
  private Server server = null;

  /**
   * The Ambari configuration.
   */
  @Inject
  Configuration configuration;

  /**
   * The Guice injector.
   */
  @Inject
  Injector injector;


  // Set the SQLProviderModule for the API providers.
  static {
    System.setProperty("provider.module.class", "org.apache.ambari.scom.SQLProviderModule");
  }

  // ----- Constants ---------------------------------------------------------

  private static final String CONTEXT_PATH = "/";

  private static final String SPRING_CONTEXT_LOCATION =
      "classpath:META-INF/spring-security.xml";

  /**
   * The logger.
   */
  private static final Logger LOG = LoggerFactory.getLogger(AmbariServer.class);


  // ----- AmbariServer ------------------------------------------------------

  public static void main(String[] args) throws Exception {
    Injector injector = Guice.createInjector(new ControllerModule());
    injector.getInstance(GuiceJpaInitializer.class);

    AmbariServer ambariServer = null;
    try {
      LOG.info("Getting the controller");
      ambariServer = injector.getInstance(AmbariServer.class);

      ComponentSSLConfiguration.instance().init(ambariServer.configuration);
      SinkConnectionFactory.instance().init(ambariServer.configuration);
      ClusterDefinitionProvider.instance().init(ambariServer.configuration);

      if (ambariServer != null) {
        ambariServer.run();
      }
    } catch (Throwable t) {
      LOG.error("Failed to run the Ambari Server", t);
      if (ambariServer != null) {
        ambariServer.stop();
      }
      System.exit(-1);
    }
  }


  // ----- helper methods ----------------------------------------------------

  // Run the server
  private void run() throws Exception {
    addInMemoryUsers();

    server = new Server();

    try {
      ClassPathXmlApplicationContext parentSpringAppContext =
          new ClassPathXmlApplicationContext();
      parentSpringAppContext.refresh();
      ConfigurableListableBeanFactory factory = parentSpringAppContext.
          getBeanFactory();
      factory.registerSingleton("guiceInjector",
          injector);
      factory.registerSingleton("passwordEncoder",
          injector.getInstance(PasswordEncoder.class));
      factory.registerSingleton("ambariLocalUserService",
          injector.getInstance(AmbariLocalUserDetailsService.class));
      factory.registerSingleton("ambariLdapAuthenticationProvider",
          injector.getInstance(AmbariLdapAuthenticationProvider.class));

      //Spring Security xml config depends on this Bean
      String[] contextLocations = {SPRING_CONTEXT_LOCATION};
      ClassPathXmlApplicationContext springAppContext = new
          ClassPathXmlApplicationContext(contextLocations, parentSpringAppContext);

      //setting ambari web context
      ServletContextHandler root = new ServletContextHandler(server, CONTEXT_PATH,
          ServletContextHandler.SECURITY | ServletContextHandler.SESSIONS);

      //Changing session cookie name to avoid conflicts
      root.getSessionHandler().getSessionManager().setSessionCookie("AMBARISESSIONID");

      GenericWebApplicationContext springWebAppContext = new GenericWebApplicationContext();
      springWebAppContext.setServletContext(root.getServletContext());
      springWebAppContext.setParent(springAppContext);
      /* Configure web app context */
      root.setResourceBase(configuration.getWebAppDir());

      root.getServletContext().setAttribute(
          WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
          springWebAppContext);

      ServletHolder rootServlet = root.addServlet(DefaultServlet.class, "/");
      rootServlet.setInitOrder(1);

      //Spring Security Filter initialization
      DelegatingFilterProxy springSecurityFilter = new DelegatingFilterProxy();
      springSecurityFilter.setTargetBeanName("springSecurityFilterChain");

      //session-per-request strategy for api
      root.addFilter(new FilterHolder(injector.getInstance(AmbariPersistFilter.class)), "/api/*", 1);

      if (configuration.getApiAuthentication()) {
        root.addFilter(new FilterHolder(springSecurityFilter), "/api/*", 1);
      }

      //Secured connector for 2-way auth
      SslSelectChannelConnector sslConnectorTwoWay = new
          SslSelectChannelConnector();
      sslConnectorTwoWay.setPort(configuration.getTwoWayAuthPort());

      Map<String, String> configsMap = configuration.getConfigsMap();
      String keystore = configsMap.get(Configuration.SRVR_KSTR_DIR_KEY) +
          File.separator + configsMap.get(Configuration.KSTR_NAME_KEY);
      String srvrCrtPass = configsMap.get(Configuration.SRVR_CRT_PASS_KEY);
      sslConnectorTwoWay.setKeystore(keystore);
      sslConnectorTwoWay.setTruststore(keystore);
      sslConnectorTwoWay.setPassword(srvrCrtPass);
      sslConnectorTwoWay.setKeyPassword(srvrCrtPass);
      sslConnectorTwoWay.setTrustPassword(srvrCrtPass);
      sslConnectorTwoWay.setKeystoreType("PKCS12");
      sslConnectorTwoWay.setTruststoreType("PKCS12");
      sslConnectorTwoWay.setNeedClientAuth(configuration.getTwoWaySsl());

      //Secured connector for 1-way auth
      SslContextFactory contextFactory = new SslContextFactory(true);
      contextFactory.setKeyStorePath(keystore);
      contextFactory.setTrustStore(keystore);
      contextFactory.setKeyStorePassword(srvrCrtPass);
      contextFactory.setKeyManagerPassword(srvrCrtPass);
      contextFactory.setTrustStorePassword(srvrCrtPass);
      contextFactory.setKeyStoreType("PKCS12");
      contextFactory.setTrustStoreType("PKCS12");

      contextFactory.setNeedClientAuth(false);
      SslSelectChannelConnector sslConnectorOneWay = new SslSelectChannelConnector(contextFactory);
      sslConnectorOneWay.setPort(configuration.getOneWayAuthPort());
      sslConnectorOneWay.setAcceptors(2);
      sslConnectorTwoWay.setAcceptors(2);

      ServletHolder sh = new ServletHolder(ServletContainer.class);
      sh.setInitParameter("com.sun.jersey.config.property.resourceConfigClass",
          "com.sun.jersey.api.core.PackagesResourceConfig");
      sh.setInitParameter("com.sun.jersey.config.property.packages",
          "org.apache.ambari.server.api.rest;" +
              "org.apache.ambari.server.api.services;" +
              "org.apache.ambari.eventdb.webservice;" +
              "org.apache.ambari.server.api");
      sh.setInitParameter("com.sun.jersey.api.json.POJOMappingFeature",
          "true");
      root.addServlet(sh, "/api/v1/*");
      sh.setInitOrder(2);


      //Set jetty thread pool
      server.setThreadPool(new QueuedThreadPool(25));

      /* Configure the API server to use the NIO connectors */
      SelectChannelConnector apiConnector;

      if (configuration.getApiSSLAuthentication()) {
        String httpsKeystore = configsMap.get(Configuration.CLIENT_API_SSL_KSTR_DIR_NAME_KEY) +
            File.separator + configsMap.get(Configuration.CLIENT_API_SSL_KSTR_NAME_KEY);
        LOG.info("API SSL Authentication is turned on. Keystore - " + httpsKeystore);

        String httpsCrtPass = configsMap.get(Configuration.CLIENT_API_SSL_CRT_PASS_KEY);

        SslSelectChannelConnector sapiConnector = new SslSelectChannelConnector();
        sapiConnector.setPort(configuration.getClientSSLApiPort());
        sapiConnector.setKeystore(httpsKeystore);
        sapiConnector.setTruststore(httpsKeystore);
        sapiConnector.setPassword(httpsCrtPass);
        sapiConnector.setKeyPassword(httpsCrtPass);
        sapiConnector.setTrustPassword(httpsCrtPass);
        sapiConnector.setKeystoreType("PKCS12");
        sapiConnector.setTruststoreType("PKCS12");
        sapiConnector.setMaxIdleTime(configuration.getConnectionMaxIdleTime());
        apiConnector = sapiConnector;
      } else {
        apiConnector = new SelectChannelConnector();
        apiConnector.setPort(configuration.getClientApiPort());
        apiConnector.setMaxIdleTime(configuration.getConnectionMaxIdleTime());
      }

      server.addConnector(apiConnector);

      server.setStopAtShutdown(true);
      springAppContext.start();

      String osType = configuration.getServerOsType();
      if (osType == null || osType.isEmpty()) {
        throw new RuntimeException(Configuration.OS_VERSION_KEY + " is not "
            + " set in the ambari.properties file");
      }

      /*
       * Start the server after controller state is recovered.
       */
      server.start();
      LOG.info("********* Started Server **********");

      server.join();
      LOG.info("Joined the Server");
    } catch (BadPaddingException bpe) {

      LOG.error("Bad keystore or private key password. " +
          "HTTPS certificate re-importing may be required.");
      throw bpe;
    } catch (BindException bindException) {

      LOG.error("Could not bind to server port - instance may already be running. " +
          "Terminating this instance.", bindException);
      throw bindException;
    }
  }

  // Creates default users and roles if in-memory database is used
  @Transactional
  private void addInMemoryUsers() {
    if (getPersistenceType(configuration) == PersistenceType.IN_MEMORY &&
        configuration.getApiAuthentication()) {
      LOG.info("In-memory database is used - creating default users");
      Users users = injector.getInstance(Users.class);

      try {
        users.createUser("admin", "admin", true, true, false);
        users.createUser("user", "user", true, false, false);
      } catch (AmbariException e) {
        throw new RuntimeException(e);
      }
    }
  }

  // Stop the server
  private void stop() throws Exception {
    try {
      server.stop();
    } catch (Exception e) {
      LOG.error("Error stopping the server", e);
    }
  }

  // get the persistence type for the given configuration
  private static PersistenceType getPersistenceType(Configuration configuration) {
    String value = configuration.getProperty(Configuration.SERVER_PERSISTENCE_TYPE_KEY);
    return value == null ? PersistenceType.IN_MEMORY : PersistenceType.fromString(value);
  }


  // ----- inner class : ControllerModule ------------------------------------

  /**
   * Used for injection purposes.
   */
  private static class ControllerModule extends AbstractModule {

    private final Configuration configuration;
    private final HostsMap hostsMap;


    // ----- Constructors ----------------------------------------------------

    /**
     * Construct a controller module.
     */
    public ControllerModule(){
      configuration = new Configuration();
      hostsMap      = new HostsMap(configuration);
    }


    // ----- AbstractModule --------------------------------------------------

    @Override
    protected void configure() {
      bind(Configuration.class).toInstance(configuration);
      bind(HostsMap.class).toInstance(hostsMap);
      bind(PasswordEncoder.class).toInstance(new StandardPasswordEncoder());

      install(buildJpaPersistModule());
      bind(Gson.class).in(Scopes.SINGLETON);
    }


    // ----- helper methods --------------------------------------------------

    // Create the JPA persistence module
    private JpaPersistModule buildJpaPersistModule() {
      PersistenceType  persistenceType  = getPersistenceType(configuration);
      JpaPersistModule jpaPersistModule = new JpaPersistModule(Configuration.JDBC_UNIT_NAME);
      Properties       properties       = new Properties();
      String           databaseDriver;
      String           databaseUrl;

      if (persistenceType == PersistenceType.LOCAL) {
        databaseDriver = configuration.getLocalDatabaseUrl();
        databaseUrl    = Configuration.JDBC_LOCAL_DRIVER;

      }
      else {
        if (persistenceType == PersistenceType.IN_MEMORY) {
          databaseDriver = Configuration.JDBC_IN_MEMROY_DRIVER;
          databaseUrl = Configuration.JDBC_IN_MEMORY_URL;
        }
        else {
          databaseDriver = configuration.getDatabaseDriver();
          databaseUrl    = configuration.getDatabaseUrl();
        }
      }

      if (databaseDriver != null && databaseUrl != null) {
        properties.setProperty("javax.persistence.jdbc.url",    databaseUrl);
        properties.setProperty("javax.persistence.jdbc.driver", databaseDriver);

        properties.setProperty("eclipselink.logging.level",  "INFO");
        properties.setProperty("eclipselink.logging.logger", "org.apache.ambari.scom.logging.JpaLogger");

        // custom jdbc properties
        Map<String, String> custom = configuration.getDatabaseCustomProperties();

        if (0 != custom.size()) {
          for (Map.Entry<String, String> entry : custom.entrySet()) {
            properties.setProperty("eclipselink.jdbc.property." + entry.getKey(),
                entry.getValue());
          }
        }

        if (persistenceType == PersistenceType.IN_MEMORY) {
          properties.setProperty("eclipselink.ddl-generation",       "drop-and-create-tables");
          properties.setProperty("eclipselink.orm.throw.exceptions", "true");
          jpaPersistModule.properties(properties);
        } else {
          properties.setProperty("javax.persistence.jdbc.user",   configuration.getDatabaseUser());
          properties.setProperty("javax.persistence.jdbc.password",
              configuration.getProperty(Configuration.SERVER_JDBC_USER_PASSWD_KEY));

          switch (configuration.getJPATableGenerationStrategy()) {
            case CREATE:
              properties.setProperty("eclipselink.ddl-generation", "create-tables");
              break;
            case DROP_AND_CREATE:
              properties.setProperty("eclipselink.ddl-generation", "drop-and-create-tables");
              break;
            default:
              break;
          }
          properties.setProperty("eclipselink.ddl-generation.output-mode", "both");
          properties.setProperty("eclipselink.create-ddl-jdbc-file-name",  "DDL-create.jdbc");
          properties.setProperty("eclipselink.drop-ddl-jdbc-file-name",    "DDL-drop.jdbc");

          jpaPersistModule.properties(properties);
        }
      }
      return jpaPersistModule;
    }
  }
}

