/*
 * This program is free software; you can redistribute it and/or modify it under the 
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software 
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this 
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html 
 * or from the Free Software Foundation, Inc., 
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * Copyright 2008 - 2009 Pentaho Corporation.  All rights reserved.
*/
package org.pentaho.pac.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.BindException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.StringTokenizer;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mortbay.jetty.Connector;
import org.mortbay.jetty.Handler;
import org.mortbay.jetty.HttpConnection;
import org.mortbay.jetty.Request;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.bio.SocketConnector;
import org.mortbay.jetty.handler.AbstractHandler;
import org.mortbay.jetty.handler.ContextHandlerCollection;
import org.mortbay.jetty.handler.ResourceHandler;
import org.mortbay.jetty.plus.jaas.JAASUserRealm;
import org.mortbay.jetty.security.Constraint;
import org.mortbay.jetty.security.ConstraintMapping;
import org.mortbay.jetty.security.SecurityHandler;
import org.mortbay.jetty.security.SslSocketConnector;
import org.mortbay.jetty.servlet.Context;
import org.mortbay.jetty.servlet.ServletHolder;
import org.pentaho.pac.server.common.ConsoleProperties;
import org.pentaho.pac.server.i18n.Messages;

public class JettyServer implements Halter, IJettyServer {
  protected Server server;
  String delimeter = null;
  String consoleHome = null;
  String callbackHandler = null;
  boolean securityEnabled = false;
  private int portNumber;
  String roles = null;
  String authLoginConfigPath = null;
  String realmName = null;
  String loginModuleName = null;
  String securitEnabledValue = null;
  private static final Log logger = LogFactory.getLog(JettyServer.class);
  private static int stopPort = 0;
  private boolean running = false;
  public static JettyServer jettyServer;
  
  public static final int DEFAULT_PORT_NUMBER = 8099;
  public static final int DEFAULT_SSL_PORT_NUMBER = 8043;
  public static final int DEFAULT_STOP_PORT_NUMBER = 8011;
  public static final String DEFAULT_DELIMETER = ","; //$NON-NLS-1$
  public static final String DEFAULT_HOSTNAME = "localhost"; //$NON-NLS-1$
  public static final String CURRENT_DIR = "."; //$NON-NLS-1$
  public static final String JETTY_HOME = "jetty.home"; //$NON-NLS-1$
  public static final String AUTH_LOGIN_CONFIG_ENV_VAR = "java.security.auth.login.config"; //$NON-NLS-1$
  public static final String DEFAULT_CALLBACK_HANDLER = "org.mortbay.jetty.plus.jaas.callback.DefaultCallbackHandler"; //$NON-NLS-1$

  public JettyServer() {
    // Get the CONSOLE_HOME Environment variable. This is required as it is needed to cofigure Jetty
    consoleHome = System.getProperty("CONSOLE_HOME", CURRENT_DIR);//$NON-NLS-1$
    // Set the jetty.home to PEHTAHO_HOME
    System.setProperty(JETTY_HOME, consoleHome);
    readConfiguration();
    server = new Server();
    setupServer();
    startServer();
    stopHandler(this, stopPort);
  }

  public static JettyServer getInstance() {
    return jettyServer;
  }

  public boolean isRunning() {
    return running;
  }

  public void stop() {

    Halter halter = new Halter(this);
    // Create the thread supplying it with the runnable object
    Thread thread = new Thread(halter);

    // Start the thread
    thread.start();
  }

  public void haltNow() {
    try {
      server.stop();
      running = false;
    } catch (Exception e) {
      logger.error(Messages.getErrorString("JettyServer.ERROR_0001_UNABLE_START_SERVER"), e); //$NON-NLS-1$
    }
  }

  private void startServer() {
    Connector connector = null;
    // Check whether ssl needs to be enabled or not
    String value = ConsoleProperties.getInstance().getProperty(ConsoleProperties.SSLENABLED);
    boolean sslEnable =  (value != null && value.length() > 0) ? Boolean.parseBoolean(value) : false;
    SslParameters sslParameters = new SslParameters(ConsoleProperties.getInstance());
    if(sslEnable) {
      connector = setupSslConnector(sslParameters);
      connector.setPort(sslParameters.getSslPort());
    }  else {
      connector = new SocketConnector();
      connector.setPort(portNumber);
    }
    String hostIP;
    String hostName;
    try {
      hostIP = InetAddress.getLocalHost().getHostAddress();
      hostName = InetAddress.getLocalHost().getHostName();
    } catch (UnknownHostException e) {
      hostIP = DEFAULT_HOSTNAME;
      hostName = DEFAULT_HOSTNAME;
    }
    
    if(connector instanceof SocketConnector) {
      ((SocketConnector) connector).setResolveNames(true);  
    }

    server.setConnectors(new Connector[] { connector });
    server.setStopAtShutdown(true);
    logger.info(Messages.getString("JettyServer.CONSOLE_STARTING")); //$NON-NLS-1$

    try {
      server.start();
      logger.info(Messages.getString("JettyServer.CONSOLE_STARTED", ((sslEnable) ? "https" : "http")//$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
          + "://" + hostName + ":" + connector.getPort(), ((sslEnable) ? "https" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
              : "http") + "://" + hostIP + ":" + connector.getPort())); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
    } catch (BindException e) {
      haltNow();
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      logger.error(Messages.getErrorString("JettyServer.ERROR_0001_UNABLE_START_SERVER"), e); //$NON-NLS-1$
    }
  }

  public String getResourceBaseName() {
    return "www/org.pentaho.pac.PentahoAdminConsole"; //$NON-NLS-1$
  }
  
  public String[] getWelcomeFiles() {
    return new String[] { "PentahoAdminConsole.html" };//$NON-NLS-1$
  }
  
  protected Context createServletContext() {
    ContextHandlerCollection contextHandlers = new ContextHandlerCollection();
    Context servletContext = new Context(contextHandlers, "/", Context.SESSIONS); //$NON-NLS-1$
    servletContext.setResourceBase( getResourceBaseName() ); 
    servletContext.setWelcomeFiles( getWelcomeFiles() ); 
    
    return servletContext;
  }
  
  public void configureResourceHandlers( Context servletContext, SecurityHandler securityHandler ) {
    ResourceHandler resources = new ResourceHandler();
    resources.setResourceBase( getResourceBaseName() );
    resources.setWelcomeFiles( getWelcomeFiles() ); 
    if (securityHandler != null) {
      server.setHandlers(new Handler[] { securityHandler, resources, servletContext });
    } else {
      server.setHandlers(new Handler[] { resources, servletContext });
    }
  }

  public SecurityHandler configureSecurityHandler() {
    SecurityHandler securityHandler = null;
    // This is required for the jetty security to locate the security configuration file
    System.setProperty(AUTH_LOGIN_CONFIG_ENV_VAR, authLoginConfigPath); 
    
    // configure security if necessary
    if (securityEnabled) {
        Constraint constraint = new Constraint();
        constraint.setName(Constraint.__BASIC_AUTH);

        // Creating the roles list
        StringTokenizer token = new StringTokenizer(roles, delimeter);
        String[] rolesList = new String[token.countTokens()];
        int i =0; 
        while(token.hasMoreTokens()) {
          rolesList[i++] = token.nextToken();
        }
        
        constraint.setRoles(rolesList);
        constraint.setAuthenticate(true);
        ConstraintMapping constraintMapping = new ConstraintMapping();
        constraintMapping.setConstraint(constraint);
        constraintMapping.setPathSpec("/*"); //$NON-NLS-1$
        JAASUserRealm realm = new JAASUserRealm(realmName);
        realm.setLoginModuleName(loginModuleName);
        realm.setCallbackHandlerClass(callbackHandler);
        securityHandler = new SecurityHandler();
        securityHandler.setUserRealm(realm);       
        securityHandler.setConstraintMappings(new ConstraintMapping[] { constraintMapping });
    }
    return securityHandler;
  }
  
  public void configureServlets( Context servletContext ) {
    // add servlets
    ServletHolder defaultServlet = new ServletHolder(new DefaultConsoleServlet("/", this)); //$NON-NLS-1$
    servletContext.addServlet(defaultServlet, "/*"); //$NON-NLS-1$
    servletContext.addServlet(defaultServlet, "/halt"); //$NON-NLS-1$
    
    ServletHolder welcomeServlet = new ServletHolder(new BrowserLocaleServlet());
    servletContext.addServlet(welcomeServlet, "/browserlocalesvc"); //$NON-NLS-1$

    ServletHolder pacsvc = new ServletHolder(new org.pentaho.pac.server.PacServiceImpl());
    servletContext.addServlet(pacsvc, "/pacsvc"); //$NON-NLS-1$

    ServletHolder schedulersvc = new ServletHolder(new org.pentaho.pac.server.SchedulerServiceImpl());
    servletContext.addServlet(schedulersvc, "/schedulersvc"); //$NON-NLS-1$

    ServletHolder subscriptionsvc = new ServletHolder(new org.pentaho.pac.server.SubscriptionServiceImpl());
    servletContext.addServlet(subscriptionsvc, "/subscriptionsvc"); //$NON-NLS-1$

    ServletHolder solutionrepositorysvc = new ServletHolder(new org.pentaho.pac.server.SolutionRepositoryServiceImpl());
    servletContext.addServlet(solutionrepositorysvc, "/solutionrepositorysvc"); //$NON-NLS-1$
    
    ServletHolder jdbcdriverdiscoveryservice = new ServletHolder(new org.pentaho.pac.server.common.JdbcDriverDiscoveryServiceImpl());
    servletContext.addServlet(jdbcdriverdiscoveryservice, "/jdbcdriverdiscoverysvc"); //$NON-NLS-1$

    ServletHolder hibernateconfigurationservice = new ServletHolder(new org.pentaho.pac.server.common.HibernateConfigurationServiceImpl());
    servletContext.addServlet(hibernateconfigurationservice, "/hibernateconfigurationsvc"); //$NON-NLS-1$

  }

  public void configureEventListeners( Context servletContext ) {
    // no-op for now
  }
  
  protected final void setupServer() {
    server = new Server();
    SecurityHandler securityHandler = configureSecurityHandler();
    Context servletContext = createServletContext();
    configureServlets( servletContext );
    configureEventListeners( servletContext );
    configureResourceHandlers( servletContext, securityHandler );
  }

  public int getPortNumber() {

    return portNumber;
  }

  public void setPortNumber(int portNumber) {

    this.portNumber = portNumber;
  }

  public static void main(String[] args) {
    jettyServer = new JettyServer();
  }

  // TODO sbarkdull, can this be deleted?
  public static class HomeHandler extends AbstractHandler {
    public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch)
        throws IOException, ServletException {
      Request base_request = (request instanceof Request) ? (Request) request : HttpConnection.getCurrentConnection()
          .getRequest();
      base_request.setHandled(true);

      response.setStatus(HttpServletResponse.SC_OK);
      response.setContentType("text/html"); //$NON-NLS-1$
      response.getWriter().println("<h1>Hello OneContext</h1>"); //$NON-NLS-1$

    }
  }

  private class Halter implements Runnable {
    // This method is called when the thread runs

    private JettyServer jettyServer;

    public Halter(JettyServer jettyServer) {
      this.jettyServer = jettyServer;
    }

    public void run() {
      logger.info(Messages.getString("JettyServer.WAITING_TO_HALT")); //$NON-NLS-1$
      try {
        Thread.sleep(3000);
      } catch (Exception e) {
        // ignore this
      }
      logger.info(Messages.getString("JettyServer.HALTING")); //$NON-NLS-1$
      jettyServer.haltNow();
    }
  }
  
  public void readConfiguration() {
      String port = ConsoleProperties.getInstance().getProperty(ConsoleProperties.CONSOLE_PORT_NUMBER);
      String stopPortNumber = ConsoleProperties.getInstance().getProperty(ConsoleProperties.STOP_PORT);

      if (port != null && port.length() > 0) {
        this.portNumber = Integer.parseInt(port);
      } else {
        this.portNumber = DEFAULT_PORT_NUMBER;
      }

      if (stopPortNumber != null && stopPortNumber.length() > 0) {
        stopPort = Integer.parseInt(stopPortNumber);
      } else {
        stopPort = DEFAULT_STOP_PORT_NUMBER;
      }

      roles = ConsoleProperties.getInstance().getProperty(ConsoleProperties.CONSOLE_SECURITY_ROLES_ALLOWED);
      String delimeterValue = ConsoleProperties.getInstance().getProperty(ConsoleProperties.CONSOLE_SECURITY_ROLE_DELIMITER);

      if (delimeterValue != null && delimeterValue.length() > 0) {
        this.delimeter = delimeterValue;
      } else {
        this.delimeter = DEFAULT_DELIMETER;
      }
      
      authLoginConfigPath = ConsoleProperties.getInstance().getProperty(ConsoleProperties.CONSOLE_SECURITY_AUTH_CONFIG_PATH);
      realmName = ConsoleProperties.getInstance().getProperty(ConsoleProperties.CONSOLE_SECURITY_REALM_NAME);
      loginModuleName = ConsoleProperties.getInstance().getProperty(ConsoleProperties.CONSOLE_SECURITY_LOGIN_MODULE_NAME);
      String securitEnabledValue = ConsoleProperties.getInstance().getProperty(ConsoleProperties.CONSOLE_SECURITY_ENABLED);
      if(securitEnabledValue != null && securitEnabledValue.length() > 0) {
        securityEnabled =  Boolean.parseBoolean(securitEnabledValue);
      }
      String callbackHandlerValue = ConsoleProperties.getInstance().getProperty(ConsoleProperties.CONSOLE_SECURITY_CALLBACK_HANDLER);
      if(callbackHandlerValue != null && callbackHandlerValue.length() > 0) {
        callbackHandler =  callbackHandlerValue;
      } else  {
        callbackHandler =  DEFAULT_CALLBACK_HANDLER;
      }
      
  }
  public void stopHandler(JettyServer jServer, int stopPort) {
    ServerSocket server = null;
    try {
      server = new ServerSocket(stopPort);
    } catch (IOException ioe) {
      logger.error("IO Error:" + ioe.getLocalizedMessage()); //$NON-NLS-1$
    }
    try {
      Socket s = server.accept();
      Thread t = new Thread(new RequestHandler(jServer, s));
      t.start();
    } catch (Exception e) {
      logger.error("IO Error:" + e.getLocalizedMessage()); //$NON-NLS-1$
    }
  }
  
  private Connector setupSslConnector(SslParameters ssl)  {
    Connector connector;
    String keyStore = ssl.getKeyStore();
    if (keyStore == null) {
        keyStore = System.getProperty("javax.net.ssl.keyStore", ""); //$NON-NLS-1$ //$NON-NLS-2$
        if (keyStore == null) {
            throw new IllegalArgumentException(Messages.getErrorString("JettyServer.ERROR_0001_KEY_STORE_MUST_BE_SET")); //$NON-NLS-1$
        }
    }

    String keyStorePassword = ssl.getKeyStorePassword();
    if (keyStorePassword == null) {
        keyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword"); //$NON-NLS-1$
        if (keyStorePassword == null) {
            throw new IllegalArgumentException(Messages.getErrorString("JettyServer.ERROR_0002_KEY_STORE_PASSWORD_MUST_BE_SET")); //$NON-NLS-1$
        }
    }
    SslSocketConnector sslConnector = new SslSocketConnector();
    sslConnector.setConfidentialPort(ssl.getSslPort());
    sslConnector.setPassword(ssl.getKeyStorePassword());
    sslConnector.setKeyPassword(ssl.getKeyPassword() != null ? ssl.getKeyPassword() : keyStorePassword);
    sslConnector.setKeystore(keyStore);
    sslConnector.setKeystoreType(ssl.getKeyStoreType());
    sslConnector.setNeedClientAuth(ssl.isNeedClientAuth());
    sslConnector.setWantClientAuth(ssl.isWantClientAuth());
    // important to set this values for selfsigned keys
    // otherwise the standard truststore of the jre is used
    sslConnector.setTruststore(ssl.getTrustStore());
    if (ssl.getTrustStorePassword() != null) {
        // check is necessary because if a null password is set
        // jetty would ask for a password on the comandline
        sslConnector.setTrustPassword(ssl.getTrustStorePassword());
    }
    sslConnector.setTruststoreType(ssl.getTrustStoreType());
    sslConnector.setResolveNames(true);
    connector = sslConnector;
    return connector;
}
  private class RequestHandler implements Runnable {
    // This method is called when the thread runs

    private JettyServer jettyServer;

    private Socket socket;

    public RequestHandler(JettyServer jettyServer, Socket socket) {
      this.jettyServer = jettyServer;
      this.socket = socket;
    }

    public void run() {
      try {
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String inputLine;
        while ((inputLine = in.readLine()) != null) {
          if (inputLine != null && inputLine.length() > 0) {
            inputLine = inputLine.trim();
            if (inputLine.equalsIgnoreCase(ConsoleProperties.STOP_ARG)) {
              logger.info(Messages.getString("JettyServer.WAITING_TO_HALT")); //$NON-NLS-1$
              
              try {
                Thread.sleep(3000);
              } catch (Exception e) {
                // ignore this
              }
              logger.info(Messages.getString("JettyServer.HALTING")); //$NON-NLS-1$
              jettyServer.haltNow();
            }
          }
        }
      } catch (IOException ioe) {
        logger.error(Messages.getErrorString("JettyServer.ERROR_0002_IO_ERROR",ioe.getLocalizedMessage())); //$NON-NLS-1$
      }
    }
  }
}

