package de.ugoe.cs.eventbench.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);
		}
	}
}
