/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/tamino/src/util/org/apache/slide/util/launcher/Launcher.java,v 1.1 2004/03/25 16:18:12 juergen Exp $
 * $Revision: 1.1 $
 * $Date: 2004/03/25 16:18:12 $
 *
 * ====================================================================
 *
 * Copyright 1999-2004 The Apache Software Foundation.
 *
 * Licensed 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.slide.util.launcher;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.slide.util.Properties;
import org.apache.slide.util.Strings;
import org.apache.slide.util.XAssertionFailed;
import org.apache.slide.util.XException;
import org.apache.slide.util.nodeset.NodeSet;

/**
 * Main class invoked by launcher. Load the launcher properties, creates a class loaded,
 * instantiates the main class and invokes it's run method.
 */
public class Launcher {
    private static final String LAUNCHER_BASEDIR = "basedir";
    
    private static final String LAUNCHER_MAIN = "main";
    private static final String LAUNCHER_CP = "cp";
    
    private static final String LAUNCHER_CP_BASE = "base";
    private static final String LAUNCHER_CP_INCLUDES = "includes";
    private static final String LAUNCHER_CP_EXCLUDES = "excludes";
    
    /**
     ** Run and System.exit. The command is specified by a system property.
     **
     ** @param args   first element has to be the launcher.properties file
     **
     ** @return 0 to indicate success.
     **/
    public static void main(String[] args) throws Throwable {
        System.exit(run(args));
    }
    
    /** Instantiates main class and invoke the run method. Same a main, but without System.exit. */
    public static int run(String[] args) throws Throwable {
        boolean debug;
        Properties props;
        File file;
        
        if (args.length == 0) {
            throw new XException("missing launcher file name");
        }
        file = new File(args[0]);
        props = loadProperties(file);
        args = Strings.cdr(args);
        if (args.length > 0 && args[0].equals("__debug")) {
            debug = true;
            args = Strings.cdr(args);
        } else {
            debug = false;
        }
        Launcher m = new Launcher(debug, props, file.getAbsoluteFile().getParentFile());
        return m.invoke(args);
    }
    
    //--
    
    private final boolean debug;
    private final Properties props;
    private final File base;
    
    private Launcher(boolean debug, Properties props, File base) {
        this.debug = debug;
        this.props = props;
        this.base = base;
    }
    

    private void debug(String str) {
        if (debug) {
            System.out.println(str);
        }
    }
    
    private int invoke(String[] args) throws Throwable {
        ClassLoader loader;
        Object tool;
        Method run;
        Map map;
        Class main;
        List classpath;
        Thread current;
        ClassLoader oldLoader;
        int result;

        debug("args " + Strings.join(args, " "));
        classpath = getClasspath();
        debug("classpath: " + classpath);
        loader = createLoader(classpath);
        current = Thread.currentThread();
        oldLoader = current.getContextClassLoader();
        current.setContextClassLoader(loader);
        try {
            main = loadClass(loader, props.get(LAUNCHER_MAIN));
            debug(main.getName() + " loaded");
            tool = createTool(main);
            debug(tool.getClass().getName() + " instantiated");
            run = getRun(main);
            debug(run.getName() + " found");
            map = new HashMap();
            props.toMap(map);
            debug("running ...");
            result = doRun(tool, run, map, args);
            debug("result: " + result);
            return result;
        } catch (Error e) {
            debug("error: " + e.getClass());
            System.out.println("classpath: " + classpath);
            throw e;
        } catch (XException e) {
            debug("XException: " + e.getClass());
            // note that this is an exception thrown in this class -- exceptions thrown by the
            // invoked code are not assignable because they are loaded in a new classloaded
            System.out.println("launcher failed: " + e.getMessage());
            System.out.println("classpath: " + classpath);
            throw e;
        } finally {
            current.setContextClassLoader(oldLoader);
        }
    }
    
    private static int doRun(Object tool, Method run, Map map, String[] args) throws Throwable {
        Object[] actuals;
        Integer result;

        actuals = new Object[] { map, args };
        try {
            result = (Integer) run.invoke(tool, actuals);
        } catch (IllegalAccessException e) {
            throw new XException("cannot invoke constructor: " + e.getMessage());
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }
        return result.intValue();
    }

    private static Properties loadProperties(File file) {
        Properties props;
        
        props = new Properties();
        props.addAll(System.getProperties());
        if (!file.isFile()) {
            throw new IllegalArgumentException("launcher file not found: " + file);
        }
        props.add(LAUNCHER_BASEDIR, file.getAbsoluteFile().getParent());
        try {
            props.load(file);
        } catch (IOException e) {
            throw new IllegalArgumentException("cannot load launcher file: " + e.getMessage());
        }
        return props;
    }
    
    private List getClasspath() {
        String[] children;
        String basedirpath;
        NodeSet set;
        String includes;
        String[] includesArray;
        String excludes;
        String[] excludesArray;
        File basedir;
        int i;
        List files;
        
        children = props.getChildren(LAUNCHER_CP);
        if (children.length == 0) {
            throw new IllegalArgumentException("empty classpath");
        }
        files = new ArrayList();
        for (i = 0; i < children.length; i++) {
            set = new NodeSet();
            basedirpath = props.get(children[i] + "." + LAUNCHER_CP_BASE);
            basedirpath = basedirpath.replace('/', File.separatorChar);
            basedir = new File(basedirpath);
            if (!basedir.isAbsolute()) {
                basedir = new File(base, basedirpath);
            }
            includes = props.getOptional(children[i] + "." + LAUNCHER_CP_INCLUDES);
            if (includes != null) {
                includesArray = Strings.split(includes, ",");
                Strings.trim(includesArray);
                set.includes(includesArray);
            }
            excludes = props.getOptional(children[i] + "." + LAUNCHER_CP_EXCLUDES);
            if (excludes != null) {
                excludesArray = Strings.split(excludes, ",");
                Strings.trim(excludesArray);
                set.excludes(excludesArray);
            }
            if (!basedir.isDirectory()) {
                throw new IllegalArgumentException("basedir: no such directory: " + basedir);
            }
            try {
                set.list(basedir, files);
            } catch (IOException e) {
                throw new XAssertionFailed("I'm working on files!", e);
            }
        }
        return files;
    }
    
    private static ClassLoader createLoader(List files) {
        URL[] urls;
        int i;
        File file;
        
        urls = new URL[files.size()];
        for (i = 0; i < urls.length; i++) {
            file = (File) files.get(i);
            if (!file.exists()) {
                throw new IllegalArgumentException("no such file or directory: " + file);
            }
            try {
                urls[i] = file.toURL();
            } catch (MalformedURLException e) {
                throw new IllegalArgumentException("malformed url: " + file);
            }
        }
        
        // no parent class loader to make things invisible
        return URLClassLoader.newInstance(urls, null);
    }
    
    public static Class loadClass(ClassLoader loader, String className) throws XException {
        try {
            return loader.loadClass(className);
        } catch (ClassNotFoundException e) {
            throw new XException("class not found: " + className);
        }
    }
    
    public static Object createTool(Class clazz) throws Throwable {
        Constructor constr;
        
        // note: clazz.newInstance is not used because it does not throw
        // InvocationTargetExceptions
        
        constr = getConstructor(clazz);
        try {
            return constr.newInstance(new Object[] {});
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        } catch (IllegalAccessException e) {
            throw new XException("cannot invoke constructor", e);
        } catch (InstantiationException e) {
            throw new XException("constructor failed", e);
        }
    }
    
    public static Constructor getConstructor(Class clazz) throws XException {
        Constructor constr;
        String prefix;
        
        prefix = clazz.getName() + "(): ";
        try {
            constr = clazz.getConstructor(new Class[] {});
        } catch (NoSuchMethodException e) {
            throw new XException(prefix + "not found");
        }
        if (!Modifier.isPublic(constr.getModifiers())) {
            throw new XException(prefix + "has to be public");
        }
        if (Modifier.isAbstract(constr.getModifiers())) {
            throw new XException(prefix + "must not be abstract");
        }
        return constr;
    }
    public static Method getRun(Class clazz) throws XException {
        Method meth;
        String prefix;
        
        prefix = clazz.getName() + ".run(Map, String[]): ";
        try {
            meth = clazz.getMethod("run", new Class[] { Map.class, String[].class });
        } catch (NoSuchMethodException e) {
            throw new XException(prefix + "not found");
        }
        if (Modifier.isStatic(meth.getModifiers())) {
            throw new XException(prefix + "must not be static");
        }
        if (!Modifier.isPublic(meth.getModifiers())) {
            throw new XException(prefix + "has to be public");
        }
        if (Modifier.isAbstract(meth.getModifiers())) {
            throw new XException(prefix + "must not be abstract");
        }
        return meth;
    }
}
