// ========================================================================
// Copyright (c) 2002 Mort Bay Consulting (Australia) Pty. Ltd.
// $Id: Main.java 4163 2006-08-24 14:23:13Z wolfgang_m $
// ========================================================================

/**
 * This is an adopted version of the corresponding classes shipped
 * with Jetty.
 */
package org.exist.start;

import java.io.BufferedReader;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

import org.exist.storage.BrokerPool;

/**
 * @author Jan Hlavaty (hlavac@code.cz)
 * @author Wolfgang Meier (meier@ifs.tu-darmstadt.de)
 * @version $Revision: 4163 $
 *          <p/>
 *          TODO:
 *          - finish possible jetty.home locations
 *          - use File.toURI.toURL() on JDK 1.4+
 *          - better handling of errors (i.e. when jetty.home cannot be autodetected...)
 *          - include entries from lib _when needed_
 */

public class Main {
    private String _classname = null;

    private String _mode = "jetty";
    private static Main exist;


    private boolean _debug = Boolean.getBoolean("exist.start.debug");

    // Stores the path to the "start.config" file that's used to configure
    // the runtime classpath.
    private String startConfigFileName = "";

    // Used to find latest version of jar files that should be added to the
    // classpath.
    private final LatestFileResolver jarFileResolver = new LatestFileResolver();

    public static void main(String[] args) {
        try {
            getMain().run(args);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * Singleton Factory Method
     */
    public static Main getMain(){
        if (exist==null) exist = new Main();
        return exist;
    }

    public String getMode() {
        return this._mode;
    }

    private Main() {
    }

    public Main(String mode) {
        this._mode = mode;
    }

    static File getDirectory(String name) {
        try {
            if (name != null) {
                File dir = new File(name).getCanonicalFile();
                if (dir.isDirectory()) {
                    return dir;
                }
            }
        } catch (IOException e) {
        }
        return null;
    }

    boolean isAvailable(String classname, Classpath classpath) {
        try {
            Class.forName(classname);
            return true;
        } catch (ClassNotFoundException e) {
        	//ignore
        }

        ClassLoader loader = classpath.getClassLoader(null);
        try {
            loader.loadClass(classname);
            return true;
        } catch (ClassNotFoundException e) {
        	//ignore
        }

        return false;
    }

    public static void invokeMain(ClassLoader classloader, String classname, String[] args)
            throws IllegalAccessException, InvocationTargetException,
            NoSuchMethodException, ClassNotFoundException {
        Class invoked_class = null;
        invoked_class = classloader.loadClass(classname);
        Class[] method_param_types = new Class[1];
        method_param_types[0] = args.getClass();
        Method main = null;
        main = invoked_class.getDeclaredMethod("main", method_param_types);
        Object[] method_params = new Object[1];
        method_params[0] = args;
        main.invoke(null, method_params);
    }

    void configureClasspath(String home, Classpath classpath, InputStream config, String[] args, String mode) {

        // Any files referenced in start.config that don't exist or cannot be resolved
        // are placed in this list.
        List invalidJars = new ArrayList();

        try {
            BufferedReader cfg = new BufferedReader(new InputStreamReader(config, "ISO-8859-1"));
            Version java_version = new Version(System.getProperty("java.version"));
            Version ver = new Version();

            // JAR's already processed
            Hashtable done = new Hashtable();
            String line = cfg.readLine();
            while (line != null) {
                try {
                    if ((line.length() > 0) && (!line.startsWith("#"))) {
                        if (_debug)
                            System.err.println(">" + line);
                        StringTokenizer st = new StringTokenizer(line);
                        String subject = st.nextToken();
                        boolean include_subject = true;
                        String condition = null;
                        while (include_subject && st.hasMoreTokens()) {
                            condition = st.nextToken();
                            if (condition.equals("never")) {
                                include_subject = false;
                            } else if (condition.equals("always")) {
                            } else if (condition.equals("available")) {
                                String class_to_check = st.nextToken();
                                include_subject &= isAvailable(class_to_check, classpath);
                            } else if (condition.equals("!available")) {
                                String class_to_check = st.nextToken();
                                include_subject &= !isAvailable(class_to_check, classpath);
                            } else if (condition.equals("java")) {
                                String operator = st.nextToken();
                                String version = st.nextToken();
                                ver.parse(version);
                                include_subject
                                        &= (operator.equals("<") && java_version.compare(ver) < 0)
                                        || (operator.equals(">") && java_version.compare(ver) > 0)
                                        || (operator.equals("<=") && java_version.compare(ver) <= 0)
                                        || (operator.equals("=<") && java_version.compare(ver) <= 0)
                                        || (operator.equals("=>") && java_version.compare(ver) >= 0)
                                        || (operator.equals(">=") && java_version.compare(ver) >= 0)
                                        || (operator.equals("==") && java_version.compare(ver) == 0)
                                        || (operator.equals("!=") && java_version.compare(ver) != 0);
                            } else if (condition.equals("nargs")) {
                                String operator = st.nextToken();
                                int number = Integer.parseInt(st.nextToken());
                                include_subject &= (operator.equals("<") && args.length < number)
                                        || (operator.equals(">") && args.length > number)
                                        || (operator.equals("<=") && args.length <= number)
                                        || (operator.equals("=<") && args.length <= number)
                                        || (operator.equals("=>") && args.length >= number)
                                        || (operator.equals(">=") && args.length >= number)
                                        || (operator.equals("==") && args.length == number)
                                        || (operator.equals("!=") && args.length != number);
                            } else if (condition.equals("mode")) {
                                String operator = st.nextToken();
                                String m = st.nextToken();
                                include_subject &= (operator.equals("==") && mode.equals(m))
                                        || (operator.equals("!=") && (!mode.equals(m)));
                            } else {
                                System.err.println("ERROR: Unknown condition: " + condition);
                            }
                        }

                        String file =
                                subject.startsWith("/") ?
                                subject.replace('/', File.separatorChar)
                                : home + File.separatorChar + subject.replace('/', File.separatorChar);

                        if (_debug)
                            System.err.println("subject="
                                    + subject
                                    + " file="
                                    + file
                                    + " condition="
                                    + condition
                                    + " include_subject="
                                    + include_subject);

                        // ok, should we include?
                        if (subject.endsWith("/*")) {
                            // directory of JAR files
                            File extdir = new File(file.substring(0, file.length() - 1));
                            File[] jars = extdir.listFiles(new FilenameFilter() {
                                public boolean accept(File dir, String name) {
                                    String namelc = name.toLowerCase();
                                    return namelc.endsWith(".jar") || name.endsWith(".zip");

                                }
                            });

                            if (jars != null) {
                                for (int i = 0; i < jars.length; i++) {
                                    String jar = jars[i].getCanonicalPath();
                                    if (!done.containsKey(jar)) {
                                        if (include_subject) {
                                            done.put(jar, jar);
                                            if (classpath.addComponent(jar) && _debug)
                                                System.err.println("Adding JAR from directory: " + jar);
                                        }
                                    }
                                }
                            }
                        } else if (subject.endsWith("/")) {
                            // class directory
                            File cd = new File(file);
                            String d = cd.getCanonicalPath();
                            if (!done.containsKey(d)) {
                                done.put(d, d);
                                if (include_subject) {
                                    if (classpath.addComponent(d) && _debug)
                                        System.err.println("Adding directory: " + d);
                                }
                            }
                        } else if (subject.toLowerCase().endsWith(".class")) {
                            // Class
                            _classname = subject.substring(0, subject.length() - 6);
                        } else {
                            // single JAR file
                            String resolvedFile = jarFileResolver.getResolvedFileName(file);

                            File f = new File(resolvedFile);
                            if (include_subject) {
                                if (!f.exists()) {
                                    invalidJars.add(f.getAbsolutePath());
                                }
                            }
                            String d = f.getCanonicalPath();
                            if (!done.containsKey(d)) {
                                if (include_subject) {
                                    done.put(d, d);
                                    if (classpath.addComponent(d) && _debug)
                                        System.err.println("Adding single JAR: " + d);
                                }
                            }
                        }
                    }
                } catch (Exception e) {
                    if (_debug) {
                        System.err.println(line);
                        e.printStackTrace();
                    }
                }
                line = cfg.readLine();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        // Print message if any files from start.config were added
        // to the classpath but they could not be found.
        if (invalidJars.size() > 0) {
            Iterator it = invalidJars.iterator();
            StringBuffer nonexistentJars = new StringBuffer();
            while (it.hasNext()) {
                String invalidJar = (String) it.next();
                nonexistentJars.append("    " + invalidJar + "\n");
            }
            System.err.println(
                "\nWARN: The following JAR file entries from '"
                + startConfigFileName + "' aren't available (this may NOT be a "
                + "problem):\n"
                + nonexistentJars
            );
        }
    }

    public void run(String[] args) {
        if (args.length > 0) {
            if (args[0].equals("client")) {
                //_classname = "org.exist.client.InteractiveClient";
                _classname = "org.exist.client.InteractiveClient";
                _mode = "client";
            } else if (args[0].equals("standalone")) {
                _classname = "org.exist.StandaloneServer";
                _mode = "standalone";
            } else if (args[0].equals("backup")) {
                _classname = "org.exist.backup.Main";
                _mode = "backup";
            } else if (args[0].equals("jetty")) {
                //_classname = "org.mortbay.jetty.Server";
                _classname = "org.exist.JettyStart";
                _mode = "jetty";
            } else if (args[0].equals("shutdown")) {
                _classname = "org.exist.ServerShutdown";
                _mode = "other";
            } else {
                _classname = args[0];
                _mode = "other";
            }
            String[] nargs = new String[args.length - 1];
            if (args.length > 1)
                System.arraycopy(args, 1, nargs, 0, args.length - 1);
            args = nargs;
        } else {
            _classname = "org.exist.client.InteractiveClient";
            _mode = "client";
        }

        if (_debug) {
            System.err.println("mode = " + _mode);
        }
        File _home_dir = detectHome();

        //TODO: more attempts here...
        
        if (_home_dir != null) {
            // if we managed to detect exist.home, store it in system property
            if (_debug)
                System.err.println("EXIST_HOME=" + System.getProperty("exist.home"));
            // DWES #### can this be removed?
            System.setProperty("exist.home", _home_dir.getPath());
            System.setProperty("user.dir", _home_dir.getPath());

            // try to find Jetty
            if (_mode.equals("jetty") | _mode.equals("cluster")) {
                File _tools_dir = new File(_home_dir.getAbsolutePath() + File.separatorChar + "tools");
                if (!_tools_dir.exists()) {
                    System.err.println("ERROR: tools directory not found in " + _home_dir.getAbsolutePath());
                    return;
                }
                String _jetty_dir = null;
                String _dirs[] = _tools_dir.list();
                for (int i = 0; i < _dirs.length; i++)
                    if (_dirs[i].startsWith("jetty")) {
                        _jetty_dir = _dirs[i];
                        break;
                    }
                if (_jetty_dir == null) {
                    System.err.println("ERROR: Jetty could not be found in " + _tools_dir.getPath());
                    return;
                }
                System.setProperty("jetty.home",
                        _tools_dir.getAbsolutePath() + File.separatorChar + _jetty_dir);
                args =
                        new String[]{
                            System.getProperty("jetty.home")
                        + File.separatorChar
                        + "etc"
                        + File.separatorChar
                        + "jetty.xml"};
            }

            // find log4j.xml
            String log4j = System.getProperty("log4j.configuration");
            if (log4j == null) {
                log4j = _home_dir.getPath() + File.separatorChar + "log4j.xml";
                File lf = new File(log4j);
                if (lf.canRead())
                    System.setProperty("log4j.configuration", lf.toURI().toASCIIString());
            }

            // clean up tempdir for Jetty...
            try {
                File tmpdir = new File(System.getProperty("java.io.tmpdir")).getCanonicalFile();
                if (tmpdir.isDirectory()) {
                    System.setProperty("java.io.tmpdir", tmpdir.getPath());
                }
            } catch (IOException e) {
            }

            Classpath _classpath = constructClasspath(_home_dir, args);
            ClassLoader cl = _classpath.getClassLoader(null);
            Thread.currentThread().setContextClassLoader(cl);

            if (_debug)
                System.err.println("TEMPDIR=" + System.getProperty("java.io.tmpdir"));

            // Invoke org.mortbay.jetty.Server.main(args) using new classloader.
            try {
                invokeMain(cl, _classname, args);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            // if not, warn user
            System.err.println("ERROR: exist.home cound not be autodetected, bailing out.");
            System.err.flush();
        }
    }

    /**
     */
    public File detectHome() {
        //--------------------
        // detect exist.home:
        //--------------------
        
        // DWES #### use Configuration.getExistHome() ?
        File _home_dir = getDirectory(System.getProperty("exist.home"));
        if (_home_dir == null) {
            // if eXist is deployed as web application, try to find WEB-INF first
            File webinf = new File("WEB-INF");
            if (_debug)
                System.err.println("trying " + webinf.getAbsolutePath());
            if (webinf.exists()) {
                File jar =
                        new File(webinf.getPath()
                        + File.separatorChar
                        + "lib"
                        + File.separatorChar
                        + "exist.jar");
                if (jar.exists())
                    try {
                        _home_dir = webinf.getCanonicalFile();
                    } catch (IOException e) {
                    }
            }
        }

        if (_home_dir == null) {
            // failed: try exist.jar in current directory
            File jar = new File("exist.jar");
            if (_debug)
                System.err.println("trying " + jar.getAbsolutePath());
            if (jar.canRead()) {
                try {
                    _home_dir = new File(".").getCanonicalFile();
                } catch (IOException e) {
                }
            }
        }

        if (_home_dir == null) {
            // failed: try ../exist.jar
            File jar = new File(".." + File.separatorChar + "exist.jar");
            if (_debug)
                System.err.println("trying " + jar.getAbsolutePath());
            if (jar.exists())
                try {
                    _home_dir = new File("..").getCanonicalFile();
                } catch (IOException e) {
                }
        }

        // searching exist.jar failed, try conf.xml to have the configuration
        // at least
        if (_home_dir == null) {
            // try conf.xml in current dir
            File jar = new File("conf.xml");
            if (_debug)
                System.err.println("trying " + jar.getAbsolutePath());
            if (jar.canRead()) {
                try {
                    _home_dir = new File(".").getCanonicalFile();
                } catch (IOException e) {
                }
            }
        }

        if (_home_dir == null) {
            // try ../conf.xml
            File jar = new File(".." + File.separatorChar + "conf.xml");
            if (_debug)
                System.err.println("trying " + jar.getAbsolutePath());
            if (jar.exists())
                try {
                    _home_dir = new File("..").getCanonicalFile();
                } catch (IOException e) {
                }
        }
        return _home_dir;
    }

    /**
     * @param args
     */
    public Classpath constructClasspath(File homeDir, String[] args) {
        // set up classpath:
        Classpath _classpath = new Classpath();

        // prefill existing paths in classpath_dirs...
        if (_debug)
            System.out.println("existing classpath = " + System.getProperty("java.class.path"));

        _classpath.addClasspath(System.getProperty("java.class.path"));

        // add JARs from ext and lib
        // be smart about it

        try {
            InputStream cpcfg = null;
            // start.config can be found in two locations.
            String configFilePath1 = "";
            String configFilePath2 = "";
            try {
                configFilePath1 = homeDir.getPath() + File.separatorChar
                    + "start.config";
                cpcfg = new java.io.FileInputStream(configFilePath1);
                startConfigFileName = configFilePath1;
            } catch (java.io.FileNotFoundException e) {
                cpcfg = null;
            }
            if (cpcfg == null) {
                if (_debug)
                    System.err.println("Configuring classpath from default resource");

                configFilePath2 = "org/exist/start/start.config";
                cpcfg = getClass().getClassLoader()
                    .getResourceAsStream(configFilePath2);
                startConfigFileName = configFilePath2;
            }
            if (cpcfg == null) {
                throw new RuntimeException(
                    "start.config not found at "
                    + configFilePath1 + " or "
                    + configFilePath2 + ", Bailing out."
                );
            }
            if (_debug) {
            	System.err.println(
            		"Configuring classpath from: " + startConfigFileName
            	);
            }
            configureClasspath(homeDir.getPath(), _classpath, cpcfg, args, _mode);
            cpcfg.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // try to find javac and add it in classpaths
        String java_home = System.getProperty("java.home");
        if (java_home != null) {
            File jdk_home = null;
            try {
                jdk_home = new File(java_home).getParentFile().getCanonicalFile();
            } catch (IOException e) {
            }
            if (jdk_home != null) {
                File tools_jar_file = null;
                try {
                    tools_jar_file =
                            new File(jdk_home, "lib" + File.separator + "tools.jar")
                            .getCanonicalFile();
                } catch (IOException e) {
                }

                if ((tools_jar_file != null) && tools_jar_file.isFile()) {
                    // OK, found tools.jar in java.home/../lib
                    // add it in
                    _classpath.addComponent(tools_jar_file);
                    if (_debug)
                        System.err.println("JAVAC = " + tools_jar_file);
                }
            }
        }

        // okay, classpath complete.
        System.setProperty("java.class.path", _classpath.toString());

        if (_debug)
            System.err.println("CLASSPATH=" + _classpath.toString());
        return _classpath;
    }

    public void shutdown() {
        BrokerPool.stopAll(false);
    }
}
