/*
 * Helma License Notice
 *
 * The contents of this file are subject to the Helma License
 * Version 2.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://adele.helma.org/download/helma/license.txt
 *
 * Copyright 1998-2003 Helma Software. All Rights Reserved.
 *
 * $RCSfile: Server.java,v $
 * $Author$
 * $Revision$
 * $Date$
 */

package helma.main;

import helma.extensions.HelmaExtension;
import helma.framework.repository.FileResource;
import helma.framework.core.*;
import helma.objectmodel.db.DbSource;
import helma.util.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlrpc.*;

import java.io.*;
import java.util.*;
import java.net.*;

import helma.util.ResourceProperties;

/**
 * Helma server main class.
 */
public class Server implements Runnable {
    // version string
    public static final String version = "🐜 (__builddate__)";

    // static server instance
    private static Server server;

    // Server home directory
    protected File hopHome;

    // server-wide properties
    ResourceProperties appsProps;
    ResourceProperties dbProps;
    ResourceProperties sysProps;

    // our logger
    private Log logger;
    // are we using helma.util.Logging?
    private boolean helmaLogging;

    // server start time
    public final long starttime;

    // if paranoid == true we only accept XML-RPC connections from
    // explicitly listed hosts.
    public boolean paranoid;
    private ApplicationManager appManager;
    private Vector extensions;
    private Thread mainThread;

    // configuration
    ServerConfig config;

    // map of server-wide database sources
    Hashtable dbSources;

    // the embedded web server
    // protected Serve websrv;
    protected JettyServer jetty;

    // the XML-RPC server
    protected WebServer xmlrpc;

    Thread shutdownhook;


    /**
     * Constructs a new Server instance with an array of command line options.
     * TODO make this a singleton
     * @param config the configuration
     */
    public Server(ServerConfig config) {
        server = this;
        starttime = System.currentTimeMillis();

        this.config = config;
        hopHome    = config.getHomeDir();
        if (hopHome == null) {
            throw new RuntimeException("helma.home property not set");
        }

        // create system properties
        sysProps = new ResourceProperties();
        if (config.hasPropFile()) {
            sysProps.addResource(new FileResource(config.getPropFile()));
        }
    }


    /**
     * Static main entry point.
     * @param args the command line arguments
     */
    public static void main(String[] args) throws IOException {
        loadServer(args);
        // parse properties files etc
        server.init();
        // start the server main thread
        server.start();
    }

    /**
     * Entry point used by launcher.jar to load a server instance
     * @param args the command line arguments
     * @return the server instance
     */
    public static Server loadServer(String[] args) {
        checkJavaVersion();

        ServerConfig config = null;
        try {
            config = getConfig(args);
        } catch (Exception cex) {
            printUsageError("error reading configuration: " + cex.getMessage());
            System.exit(1);
        }

        checkRunning(config);

        // create new server instance
        server = new Server(config);
        return server;
    }


    /**
      * check if we are running on a Java 2 VM - otherwise exit with an error message
      */
    public static void checkJavaVersion() {
        String javaVersion = System.getProperty("java.version");

        if ((javaVersion == null) || javaVersion.startsWith("1.5")
                                  || javaVersion.startsWith("1.4")
                                  || javaVersion.startsWith("1.3")
                                  || javaVersion.startsWith("1.2")
                                  || javaVersion.startsWith("1.1")
                                  || javaVersion.startsWith("1.0")) {
            System.err.println("This version of Helma requires Java 1.6 or greater.");

            if (javaVersion == null) { // don't think this will ever happen, but you never know
                System.err.println("Your Java Runtime did not provide a version number. Please update to a more recent version.");
            } else {
                System.err.println("Your Java Runtime is version " + javaVersion +
                                   ". Please update to a more recent version.");
            }

            System.exit(1);
        }
    }


    /**
      * parse the command line arguments, read a given server.properties file
      * and check the values given for server ports
      * @return ServerConfig if successfull
      * @throws Exception on any configuration error
      */
    public static ServerConfig getConfig(String[] args) throws Exception {

        ServerConfig config = new ServerConfig();

        // get possible environment setting for helma home
        if (System.getProperty("helma.home")!=null) {
            config.setHomeDir(new File(System.getProperty("helma.home")));
        }

        parseArgs(config, args);

        guessConfig(config);

        // create system properties
        ResourceProperties sysProps = new ResourceProperties();
        sysProps.addResource(new FileResource(config.getPropFile()));

        // check if there's a property setting for those ports not specified via command line
        if (!config.hasWebsrvPort() && sysProps.getProperty("webPort") != null) {
            try {
                config.setWebsrvPort(getInetSocketAddress(sysProps.getProperty("webPort")));
            } catch (Exception portx) {
                throw new Exception("Error parsing web server port property from server.properties: " + portx);
            }
        }

        if (!config.hasXmlrpcPort() && sysProps.getProperty("xmlrpcPort") != null) {
            try {
                config.setXmlrpcPort(getInetSocketAddress(sysProps.getProperty("xmlrpcPort")));
            } catch (Exception portx) {
                throw new Exception("Error parsing XML-RPC server port property from server.properties: " + portx);
            }
        }
        return config;
    }


    /**
      * parse argument list from command line and store values
      * in given ServerConfig object
      * @throws Exception when argument can't be parsed into an InetAddrPort
      * or invalid token is given.
      */
    public static void parseArgs(ServerConfig config, String[] args) throws Exception {
        for (int i = 0; i < args.length; i++) {
            if (args[i].equals("-h") && ((i + 1) < args.length)) {
                config.setHomeDir(new File(args[++i]));
            } else if (args[i].equals("-f") && ((i + 1) < args.length)) {
                config.setPropFile(new File(args[++i]));
            } else if (args[i].equals("-a") && ((i + 1) < args.length)) {
                config.setApps(StringUtils.split(args[++i]));
            } else if (args[i].equals("-x") && ((i + 1) < args.length)) {
                try {
                    config.setXmlrpcPort(getInetSocketAddress(args[++i]));
                } catch (Exception portx) {
                    throw new Exception("Error parsing XML-RPC server port property: " + portx);
                }
            } else if (args[i].equals("-w") && ((i + 1) < args.length)) {
                try {
                    config.setWebsrvPort(getInetSocketAddress(args[++i]));
                } catch (Exception portx) {
                    throw new Exception("Error parsing web server port property: " + portx);
                }
            } else if (args[i].equals("-c") && ((i + 1) < args.length)) {
                config.setConfigFile(new File(args[++i]));
            } else if (args[i].equals("-i") && ((i + 1) < args.length)) {
                // eat away the -i parameter which is meant for helma.main.launcher.Main
                i++;
            } else {
                throw new Exception("Unknown command line token: " + args[i]);
            }
        }
    }


    /**
      * get main property file from home dir or vice versa,
      * depending on what we have
      */
    public static void guessConfig(ServerConfig config) throws Exception {
        // get property file from hopHome:
        if (!config.hasPropFile()) {
            if (config.hasHomeDir()) {
                config.setPropFile(new File(config.getHomeDir(), "server.properties"));
            } else {
                config.setPropFile(new File("server.properties"));
            }
        }

        // create system properties
        ResourceProperties sysProps = new ResourceProperties();
        sysProps.addResource(new FileResource(config.getPropFile()));

        // try to get hopHome from property file
        if (!config.hasHomeDir() && sysProps.getProperty("hophome") != null) {
            config.setHomeDir(new File(sysProps.getProperty("hophome")));
        }

        // use the directory where server.properties is located:
        if (!config.hasHomeDir() && config.hasPropFile()) {
            config.setHomeDir(config.getPropFile().getAbsoluteFile().getParentFile());
        }

        if (!config.hasPropFile()) {
            throw new Exception ("no server.properties found");
        }

        if (!config.hasHomeDir()) {
            throw new Exception ("couldn't determine helma directory");
        }
    }


    /**
      * print the usage hints and prefix them with a message.
      */
    public static void printUsageError(String msg) {
        System.out.println(msg);
        printUsageError();
    }


    /**
      * print the usage hints
      */
    public static void printUsageError() {
        System.out.println("");
        System.out.println("Usage: java helma.main.Server [options]");
        System.out.println("Possible options:");
        System.out.println("  -a app[,...]      Specify applications to start");
        System.out.println("  -h dir            Specify hop home directory");
        System.out.println("  -f file           Specify server.properties file");
        System.out.println("  -c jetty.xml      Specify Jetty XML configuration file");
        System.out.println("  -w [ip:]port      Specify embedded web server address/port");
        System.out.println("  -x [ip:]port      Specify XML-RPC address/port");
        System.out.println("");
        System.out.println("Supported formats for server ports:");
        System.out.println("   <port-number>");
        System.out.println("   <ip-address>:<port-number>");
        System.out.println("   <hostname>:<port-number>");
        System.out.println("");
        System.err.println("Usage Error - exiting");
        System.out.println("");
    }



    /**
     *  Check wheter a server is already running on any of the given ports
     *  - otherwise exit with an error message
     */
    public static void checkRunning(ServerConfig config) {
        // check if any of the specified server ports is in use already
        try {
            if (config.hasWebsrvPort()) {
                checkPort(config.getWebsrvPort());
            }

            if (config.hasXmlrpcPort()) {
                checkPort(config.getXmlrpcPort());
            }

        } catch (Exception running) {
            System.out.println(running.getMessage());
            System.exit(1);
        }

    }


    /**
     *  Check whether a server port is available by trying to open a server socket
     */
    private static void checkPort(InetSocketAddress endpoint) throws IOException {
        try {
            ServerSocket sock = new ServerSocket();
            sock.bind(endpoint);
            sock.close();
        } catch (IOException x) {
            throw new IOException("Error binding to " + endpoint + ": " + x.getMessage());
        }
    }


    /**
      * initialize the server
      */
    public void init() throws IOException {

        // set the log factory property
        String logFactory = sysProps.getProperty("loggerFactory",
                                                 "helma.util.Logging");

        helmaLogging = "helma.util.Logging".equals(logFactory);
        System.setProperty("org.apache.commons.logging.LogFactory", logFactory);

        // set the current working directory to the helma home dir.
        // note that this is not a real cwd, which is not supported
        // by java. It makes sure relative to absolute path name
        // conversion is done right, so for Helma code, this should work.
        System.setProperty("user.dir", hopHome.getPath());

        // from now on it's safe to call getLogger() because hopHome is set up
        getLogger();

        String startMessage = "Starting Helma " + version + " on Java " +
                              System.getProperty("java.version");

        logger.info(startMessage);

        // also print a msg to System.out
        System.out.println(startMessage);

        logger.info("Setting Helma Home to " + hopHome);


        // read db.properties file in helma home directory
        String dbPropfile = sysProps.getProperty("dbPropFile");
        File file;
        if ((dbPropfile != null) && !"".equals(dbPropfile.trim())) {
            file = new File(dbPropfile);
        } else {
            file = new File(hopHome, "db.properties");
        }

        dbProps = new ResourceProperties();
        dbProps.setIgnoreCase(false);
        dbProps.addResource(new FileResource(file));
        DbSource.setDefaultProps(dbProps);

        // read apps.properties file
        String appsPropfile = sysProps.getProperty("appsPropFile");
        if ((appsPropfile != null) && !"".equals(appsPropfile.trim())) {
            file = new File(appsPropfile);
        } else {
            file = new File(hopHome, "apps.properties");
        }
        appsProps = new ResourceProperties();
        appsProps.setIgnoreCase(true);
        appsProps.addResource(new FileResource(file));

        paranoid = "true".equalsIgnoreCase(sysProps.getProperty("paranoid"));

        String language = sysProps.getProperty("language");
        String country = sysProps.getProperty("country");
        String timezone = sysProps.getProperty("timezone");

        if ((language != null) && (country != null)) {
            Locale.setDefault(new Locale(language, country));
        }

        if (timezone != null) {
            TimeZone.setDefault(TimeZone.getTimeZone(timezone));
        }

        // logger.debug("Locale = " + Locale.getDefault());
        // logger.debug("TimeZone = " +
        //                 TimeZone.getDefault().getDisplayName(Locale.getDefault()));

        dbSources = new Hashtable();

        // try to load the extensions
        extensions = new Vector();
        if (sysProps.getProperty("extensions") != null) {
            initExtensions();
        }
        jetty = JettyServer.init(this, config);
    }


    /**
      * initialize extensions
      */
    private void initExtensions() {
        StringTokenizer tok = new StringTokenizer(sysProps.getProperty("extensions"), ",");
        while (tok.hasMoreTokens()) {
            String extClassName = tok.nextToken().trim();

            try {
                Class extClass = Class.forName(extClassName);
                HelmaExtension ext = (HelmaExtension) extClass.newInstance();
                ext.init(this);
                extensions.add(ext);
                logger.info("Loaded: " + extClassName);
            } catch (Throwable e) {
                logger.error("Error loading extension " + extClassName + ": " + e.toString());
            }
        }
    }



    public void start() {
        // Start running, finishing setup and then entering a loop to check changes
        // in the apps.properties file.
        mainThread = new Thread(this);
        mainThread.start();
    }

    public void stop() {
        mainThread = null;
        appManager.stopAll();
    }

    public void shutdown() {
        getLogger().info("Shutting down Helma");

        if (jetty != null) {
            try {
                jetty.stop();
                jetty.destroy();
            } catch (Exception x) {
                // exception in jettx stop. ignore.
            }
        }

        if (xmlrpc != null) {
            try {
                xmlrpc.shutdown();
            } catch (Exception x) {
                // exception in xmlrpc server shutdown, ignore.
            }
        }

        if (helmaLogging) {
            Logging.shutdown();
        }

        server = null;

        try {
            Runtime.getRuntime().removeShutdownHook(shutdownhook);
            // HACK: running the shutdownhook seems to be necessary in order
            // to prevent it from blocking garbage collection of helma
            // classes/classloaders. Since we already set server to null it
            // won't do anything anyhow.
            shutdownhook.start();
            shutdownhook = null;
        } catch (Exception x) {
            // invalid shutdown hook or already shutting down. ignore.
        }
    }

    /**
     *  The main method of the Server. Basically, we set up Applications and than
     *  periodically check for changes in the apps.properties file, shutting down
     *  apps or starting new ones.
     */
    public void run() {
        try {
            if (config.hasXmlrpcPort()) {
                InetSocketAddress xmlrpcPort = config.getXmlrpcPort();
                String xmlparser = sysProps.getProperty("xmlparser");

                if (xmlparser != null) {
                    XmlRpc.setDriver(xmlparser);
                }

                if (xmlrpcPort.getAddress() != null) {
                    xmlrpc = new WebServer(xmlrpcPort.getPort(), xmlrpcPort.getAddress());
                } else {
                    xmlrpc = new WebServer(xmlrpcPort.getPort());
                }

                if (paranoid) {
                    xmlrpc.setParanoid(true);

                    String xallow = sysProps.getProperty("allowXmlRpc");

                    if (xallow != null) {
                        StringTokenizer st = new StringTokenizer(xallow, " ,;");

                        while (st.hasMoreTokens())
                            xmlrpc.acceptClient(st.nextToken());
                    }
                }
                xmlrpc.start();
                logger.info("Starting XML-RPC server on port " + (xmlrpcPort));
            }

            appManager = new ApplicationManager(appsProps, this);

            if (xmlrpc != null) {
                xmlrpc.addHandler("$default", appManager);
            }

            // add shutdown hook to close running apps and servers on exit
            shutdownhook = new HelmaShutdownHook();
            Runtime.getRuntime().addShutdownHook(shutdownhook);
        } catch (Exception x) {
            throw new RuntimeException("Error setting up Server", x);
        }

        // set the security manager.
        // the default implementation is helma.main.HelmaSecurityManager.
        try {
            String secManClass = sysProps.getProperty("securityManager");

            if (secManClass != null) {
                SecurityManager secMan = (SecurityManager) Class.forName(secManClass)
                                                                .newInstance();

                System.setSecurityManager(secMan);
                logger.info("Setting security manager to " + secManClass);
            }
        } catch (Exception x) {
            logger.error("Error setting security manager", x);
        }

        // start applications
        appManager.startAll();

        // start embedded web server
        if (jetty != null) {
            try {
                jetty.start();
            } catch (Exception m) {
                throw new RuntimeException("Error starting embedded web server", m);
            }
        }

        while (Thread.currentThread() == mainThread) {
            try {
                Thread.sleep(3000L);
            } catch (InterruptedException ie) {
            }

            try {
                appManager.checkForChanges();
            } catch (Exception x) {
                logger.warn("Caught in app manager loop: " + x);
            }
        }
    }

    /**
     * Make sure this server has an ApplicationManager (e.g. used when
     * accessed from CommandlineRunner)
     */
    public void checkAppManager() {
        if (appManager == null) {
            appManager = new ApplicationManager(appsProps, this);
        }
    }

    /**
     *  Get an Iterator over the applications currently running on this Server.
     */
    public Object[] getApplications() {
        return appManager.getApplications();
    }

    /**
     * Get an Application by name
     */
    public Application getApplication(String name) {
        return appManager.getApplication(name);
    }

    /**
     *  Get a logger to use for output in this server.
     */
    public Log getLogger() {
        if (logger == null) {
            if (helmaLogging) {
                // set up system properties for helma.util.Logging
                String logDir = sysProps.getProperty("logdir", "log");

                if (!"console".equals(logDir)) {
                    // try to get the absolute logdir path

                    // set up helma.logdir system property
                    File dir = new File(logDir);
                    if (!dir.isAbsolute()) {
                        dir = new File(hopHome, logDir);
                    }

                    logDir = dir.getAbsolutePath();
                }
                System.setProperty("helma.logdir", logDir);
            }
            logger = LogFactory.getLog("helma.server");
        }

        return logger;
    }

    /**
     *  Get the Home directory of this server.
     */
    public File getHopHome() {
        return hopHome;
    }

    /**
     * Get the explicit list of apps if started with -a option
     * @return
     */
    public String[] getApplicationsOption() {
        return config.getApps();
    }

    /**
     * Get the main Server instance.
     */
    public static Server getServer() {
        return server;
    }

    /**
     *  Get the Server's  XML-RPC web server.
     */
    public static WebServer getXmlRpcServer() {
        return server.xmlrpc;
    }

    /**
     *
     *
     * @param key ...
     *
     * @return ...
     */
    public String getProperty(String key) {
        return (String) sysProps.get(key);
    }

    /**
     * Return the server.properties for this server
     * @return the server.properties
     */
    public ResourceProperties getProperties() {
        return sysProps;
    }

    /**
     * Return the server-wide db.properties
     * @return the server-wide db.properties
     */
    public ResourceProperties getDbProperties() {
        return dbProps;
    }

    /**
     * Return the apps.properties entries for a given application
     * @param appName the app name
     * @return the apps.properties subproperties for the given app
     */
    public ResourceProperties getAppsProperties(String appName) {
        if (appName == null) {
            return appsProps;
        } else {
            return appsProps.getSubProperties(appName + ".");
        }
    }

    /**
     *
     *
     * @return ...
     */
    public File getAppsHome() {
        String appHome = sysProps.getProperty("appHome", "");

        if (appHome.trim().length() != 0) {
            return new File(appHome);
        } else {
            return new File(hopHome, "apps");
        }
    }

    /**
     *
     *
     * @return ...
     */
    public File getDbHome() {
        String dbHome = sysProps.getProperty("dbHome", "");

        if (dbHome.trim().length() != 0) {
            return new File(dbHome);
        } else {
            return new File(hopHome, "db");
        }
    }

    /**
     *
     *
     * @return ...
     */
    public Vector getExtensions() {
        return extensions;
    }

    /**
     *
     *
     * @param name ...
     */
    public void startApplication(String name) {
        appManager.start(name);
        appManager.register(name);
    }

    /**
     *
     *
     * @param name ...
     */
    public void stopApplication(String name) {
        appManager.stop(name);
    }

    private static InetSocketAddress getInetSocketAddress(String inetAddrPort)
            throws UnknownHostException {
        InetAddress addr = null;
        int c = inetAddrPort.indexOf(':');
        if (c >= 0) {
            String a = inetAddrPort.substring(0, c);
            if (a.indexOf('/') > 0)
                a = a.substring(a.indexOf('/') + 1);
            inetAddrPort = inetAddrPort.substring(c + 1);

            if (a.length() > 0 && !"0.0.0.0".equals(a)) {
                addr = InetAddress.getByName(a);
            }
        }
        int port = Integer.parseInt(inetAddrPort);
        return new InetSocketAddress(addr, port);
    }
}


