//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   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 de.ugoe.cs.autoquest.jfcmonitor;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;

/**
 * <p>
 * Class that launches an executable Jar-file in the same thread and VM where the JarLauncher
 * instance is created. The requirements on the Jar file are:
 * <li>Must contain a MANIFEST.</li>
 * <li>The MANIFEST must define the main-class of the application ("Main-Class" entry).</li>
 * <li>The MANIFEST must define the classpath of the application ("Class-Path" entry).</li>
 * </p>
 * 
 * @author Steffen Herbold
 * @version 1.0
 */
public class JarLauncher {

    /**
     * <p>
     * Name of the Jar file to be executed.
     * </p>
     */
    private String jarfile;

    /**
     * <p>
     * Arguments for launching the Jar file.
     * </p>
     */
    private String[] args;

    /**
     * <p>
     * Helper variable with the path to the working directory.
     * </p>
     */
    final String workingDir = System.getProperty("user.dir") + "/";

    /**
     * <p>
     * Internal variable used to store the classpath extracted from the Jar file's MANIFEST.
     * </p>
     */
    private String[] classPath = new String[] { };

    /**
     * <p>
     * Internal variable used to store the name (including package information) of the Jar file's
     * main function extracted from the Jar file's MANIFEST.
     * </p>
     */
    private String mainClassName = "";

    /**
     * <p>
     * Inner class that defines an exception that is thrown if launching the application in the Jar
     * file fails.
     * </p>
     * 
     * @author Steffen Herbold
     * @version 1.0
     */
    private static class JarLaunchException extends Exception {

        /**
         * <p>
         * Id for object serialization.
         * </p>
         */
        private static final long serialVersionUID = 1L;

        /**
         * <p>
         * Constructor. Creates a new JarLaunchException.
         * </p>
         * 
         * @param string
         *            error message of the exception
         */
        public JarLaunchException(String string) {
            super(string);
        }

        /**
         * <p>
         * Constructor. Creates a new JarLaunchException as a copy of an existing exception.
         * </p>
         * 
         * @param e
         *            exception that is copied
         */
        public JarLaunchException(Exception e) {
            super(e);
        }

    }

    /**
     * <p>
     * Constructor. Creates a new JarLauncher.
     * </p>
     * 
     * @param jarfile
     *            file to be launched; must not be complete path but in relation to the current
     *            working directory
     * @param args
     *            arguments with which the main function of the Jar file is called
     */
    public JarLauncher(String jarfile, String[] args) {
        this.jarfile = jarfile;
        this.args = Arrays.copyOf(args, args.length);
    }

    /**
     * <p>
     * Executes the main function of the Jar file associated with this launcher.
     * </p>
     */
    public void exec() {
        try {
            getInfoFromJar();
            initClassLoader();
            runMain();
        }
        catch (JarLaunchException e) {
            System.err.println("Failure to launch application.");
            System.err.println(e.getMessage());
        }
    }

    /**
     * <p>
     * Retrieves the classpath and main function from the Jar file's MANIFEST.
     * </p>
     * 
     * @throws JarLaunchException
     *             thrown if reading of Jar file or MANIFEST fails
     */
    private void getInfoFromJar() throws JarLaunchException {
        JarInputStream jarInputStream;
        try {
            jarInputStream = new JarInputStream(new FileInputStream(workingDir + jarfile));
        }
        catch (FileNotFoundException e) {
            throw new JarLaunchException(e);
        }
        catch (IOException e) {
            throw new JarLaunchException(e);
        }
        Manifest manifest = jarInputStream.getManifest();
        mainClassName = manifest.getMainAttributes().getValue("Main-Class");
        String jarClassPath = manifest.getMainAttributes().getValue("Class-Path");
        String[] jarClassPathElements = jarClassPath.split(" ");
        classPath = new String[jarClassPathElements.length];
        for (int i = 0; i < jarClassPathElements.length; i++) {
            classPath[i] = "file:" + workingDir + jarClassPathElements[i];
        }
        try {
            jarInputStream.close();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * <p>
     * Modifies the {@link ClassLoader} of the current VM such that it includes the class path
     * defined in the Jar file's MANIFEST.
     * </p>
     * 
     * @throws JarLaunchException
     *             thrown if modification of {@link ClassLoader} fails.
     */
    private void initClassLoader() throws JarLaunchException {
        URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
        Method method;
        try {
            method = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]
                { URL.class });
        }
        catch (SecurityException e) {
            throw new JarLaunchException(
                                         "addURL method of URLClassLoader not accessible via reflection.");
        }
        catch (NoSuchMethodException e) {
            throw new JarLaunchException(
                                         "URLClassLoader does not have addURL method. Should be impossible!!");
        }
        method.setAccessible(true);

        try {
            method.invoke(classLoader, new Object[]
                { new URL("file:" + workingDir + jarfile) });
            for (String element : classPath) {
                method.invoke(classLoader, new Object[]
                    { new URL(element) });
            }
        }
        catch (IllegalArgumentException e) {
            throw new JarLaunchException(
                                         "Illegal arguments for addURL method. Should be impossible!!");
        }
        catch (MalformedURLException e) {
            throw new JarLaunchException(e);
        }
        catch (IllegalAccessException e) {
            throw new JarLaunchException(
                                         "addURL method of URLClassLoader not accessible via reflection.");
        }
        catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    /**
     * <p>
     * Executes the main function.
     * </p>
     * 
     * @throws JarLaunchException
     *             thrown if execution of main function fails or the main function itself throws an
     *             exception
     */
    private void runMain() throws JarLaunchException {
        Class<?> mainClass;
        try {
            mainClass = Class.forName(mainClassName);
        }
        catch (ClassNotFoundException e) {
            throw new JarLaunchException("Main class not found: " + mainClassName);
        }
        Method mainMethod;
        try {
            mainMethod = mainClass.getMethod("main", new Class[]
                { String[].class });
        }
        catch (SecurityException e) {
            throw new JarLaunchException("Main method not accessible.");
        }
        catch (NoSuchMethodException e) {
            throw new JarLaunchException("Main method not found.");
        }
        try {
            mainMethod.invoke(null, new Object[]
                { args });
        }
        catch (IllegalArgumentException e) {
            throw new JarLaunchException(
                                         "Illegal arguments for main method. Should be impossible!!");
        }
        catch (IllegalAccessException e) {
            throw new JarLaunchException("Main method not accessible.");
        }
        catch (InvocationTargetException e) {
            throw new JarLaunchException(e);
        }
    }
}
