/*
 * 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.commons.launcher;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLDecoder;
import java.util.ResourceBundle;

import org.apache.commons.launcher.types.ArgumentSet;
import org.apache.commons.launcher.types.JVMArgumentSet;
import org.apache.commons.launcher.types.SysPropertySet;
import org.apache.tools.ant.Main;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
import org.apache.tools.ant.taskdefs.Ant;
import org.apache.tools.ant.taskdefs.Available;
import org.apache.tools.ant.taskdefs.CallTarget;
import org.apache.tools.ant.taskdefs.ConditionTask;
import org.apache.tools.ant.taskdefs.Exit;
import org.apache.tools.ant.taskdefs.Property;
import org.apache.tools.ant.taskdefs.Mkdir;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.taskdefs.Delete;
import org.apache.tools.ant.types.Description;
import org.apache.tools.ant.types.FileList;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.PatternSet;

/**
 * A class that is used to launch a Java process. The primary purpose of this
 * class is to eliminate the need for a batch or shell script to launch a Java
 * process. Some situations where elimination of a batch or shell script may be 
 * desirable are:
 * <ul>
 * <li>You want to avoid having to determining where certain application paths
 *  are e.g. your application's home directory, etc. Determining this
 *  dynamically in a Windows batch scripts is very tricky on some versions of
 *  Windows or when softlinks are used on Unix platforms.
 * <li>You need to enforce certain properties e.g. java.endorsed.dirs when
 *  running with JDK 1.4.
 * <li>You want to allow users to pass in custom JVM arguments or system
 *  properties without having to parse and reorder arguments in your script.
 *  This can be tricky and/or messy in batch and shell scripts.
 * <li>You want to bootstrap Java properties from a configuration file instead
 *  hard-coding them in your batch and shell scripts.
 * <li>You want to provide localized error messages which is very tricky to do
 *  in batch and shell scripts.
 * </ul>
 *
 * @author Patrick Luby
 */
public class Launcher implements Runnable {

    //----------------------------------------------------------- Static Fields


    /**
     * Cached bootstrap file.
     */
    private static File bootstrapFile = null;

    /**
     * Cached java command
     */
    private static String javaCmd = null;

    /**
     * Cached JDB command
     */
    private static String jdbCmd = null;

    /**
     * Default XML file name
     */
    private final static String DEFAULT_XML_FILE_NAME = "launcher.xml";

    /**
     * Shared lock.
     */
    private static Object lock = new Object();

    /**
     * Cached log
     */
    private static PrintStream log = System.err;

    /**
     * Cached resourceBundle
     */
    private static ResourceBundle resourceBundle = null;

    /**
     * The started status flag.
     */
    private static boolean started = false;

    /**
     * The stopped status flag.
     */
    private static boolean stopped = false;

    /**
     * List of supported Ant tasks.
     */
    public final static Object[] SUPPORTED_ANT_TASKS = new Object[] {
            LaunchTask.TASK_NAME, LaunchTask.class,
            "ant", Ant.class,
            "antcall", CallTarget.class,
            "available", Available.class,
            "condition", ConditionTask.class,
            "fail", Exit.class,
            "property", Property.class,
            "mkdir", Mkdir.class,
            "delete", Delete.class,
            "copy", Copy.class
        };

    /**
     * List of supported Ant types.
     */
    public final static Object[] SUPPORTED_ANT_TYPES = new Object[] {
            ArgumentSet.TYPE_NAME, ArgumentSet.class,
            JVMArgumentSet.TYPE_NAME, JVMArgumentSet.class,
            SysPropertySet.TYPE_NAME, SysPropertySet.class,
            "description", Description.class,
            "fileset", FileSet.class,
            "filelist", FileList.class,
            "path", Path.class,
            "patternset", PatternSet.class
        };

    /**
     * Cached tools classpath.
     */
    private static String toolsClasspath = null;

    /**
     * The verbose flag
     */
    private static boolean verbose = false;

    //---------------------------------------------------------- Static Methods


    /**
     * Get the started flag.
     *
     * @return the value of the started flag
     */
    public static synchronized boolean isStarted() {

        return Launcher.started;

    }

    /**
     * Get the stopped flag.
     *
     * @return the value of the stopped flag
     */
    public static synchronized boolean isStopped() {

        return Launcher.stopped;

    }

    /**
     * Start the launching process. This method is essential the
     * <code>main(String[])<code> method for this class except that this method
     * never invokes {@link System#exit(int)}. This method is designed for
     * applications that wish to invoke this class directly from within their
     * application's code.
     *
     * @param args command line arguments
     * @return the exit value of the last synchronous child JVM that was
     *  launched or 1 if any other error occurs
     * @throws IllegalArgumentException if any error parsing the args parameter
     *  occurs
     */
    public static int start(String[] args) throws IllegalArgumentException {

        // Check make sure that neither this method or the stop() method is
        // already running since we do not support concurrency
        synchronized (Launcher.lock) {
            if (Launcher.isStarted() || Launcher.isStopped())
                return 1;
            Launcher.setStarted(true);
        }

        int returnValue = 0;
        ClassLoader parentLoader = null;
        Thread shutdownHook = new Thread(new Launcher());
        Runtime runtime = Runtime.getRuntime();

        try {

            // Cache the current class loader for this thread and set the class
            // loader before running Ant. Note that we only set the class loader
            // if we are running a Java version earlier than 1.4 as on 1.4 this
            // causes unnecessary loading of the XML parser classes.
            parentLoader = Thread.currentThread().getContextClassLoader();
            boolean lessThan14 = true;
            try {
                Class.forName("java.lang.CharSequence");
                lessThan14 = false;
            } catch (ClassNotFoundException cnfe) {
                // If this class does not exist, then we are not running Java 1.4
            }
            if (lessThan14)
                Thread.currentThread().setContextClassLoader(Launcher.class.getClassLoader());

            Project project = new Project();

            // Set the project's class loader
            project.setCoreLoader(Launcher.class.getClassLoader());

            // Initialize the project. Note that we don't invoke the
            // Project.init() method directly as this will cause all of
            // the myriad of Task subclasses to load which is a big
            // performance hit. Instead, we load only the
            // Launcher.SUPPORTED_ANT_TASKS and Launcher.SUPPORTED_ANT_TYPES
            // into the project that the Launcher supports.
            for (int i = 0; i < Launcher.SUPPORTED_ANT_TASKS.length; i++) {
                // The even numbered elements should be the task name
                String taskName = (String)Launcher.SUPPORTED_ANT_TASKS[i];
                // The odd numbered elements should be the task class
                Class taskClass = (Class)Launcher.SUPPORTED_ANT_TASKS[++i];
                project.addTaskDefinition(taskName, taskClass);
            }
            for (int i = 0; i < Launcher.SUPPORTED_ANT_TYPES.length; i++) {
                // The even numbered elements should be the type name
                String typeName = (String)Launcher.SUPPORTED_ANT_TYPES[i];
                // The odd numbered elements should be the type class
                Class typeClass = (Class)Launcher.SUPPORTED_ANT_TYPES[++i];
                project.addDataTypeDefinition(typeName, typeClass);
            }

            // Add all system properties as project properties
            project.setSystemProperties();

            // Parse the arguments
            int currentArg = 0;

            // Set default XML file
            File launchFile = new File(Launcher.getBootstrapDir(), Launcher.DEFAULT_XML_FILE_NAME);

            // Get standard launcher arguments
            for ( ; currentArg < args.length; currentArg++) {
                // If we find a "-" argument or an argument without a
                // leading "-", there are no more standard launcher arguments
                if ("-".equals(args[currentArg])) {
                    currentArg++;
                    break;
                } else if (args[currentArg].length() > 0 && !"-".equals(args[currentArg].substring(0, 1))) {
                    break;
                } else if ("-help".equals(args[currentArg])) {
                    throw new IllegalArgumentException();
                } else if ("-launchfile".equals(args[currentArg])) {
                    if (currentArg + 1 < args.length){
                        String fileArg = args[++currentArg];
                        launchFile = new File(fileArg);
                        if (!launchFile.isAbsolute())
                            launchFile = new File(Launcher.getBootstrapDir(), fileArg);
                    } else {
                        throw new IllegalArgumentException(args[currentArg] + " " + Launcher.getLocalizedString("missing.arg"));
                    }
                } else if ("-executablename".equals(args[currentArg])) {
                    if (currentArg + 1 < args.length)
                        System.setProperty(ChildMain.EXECUTABLE_PROP_NAME, args[++currentArg]);
                    else
                        throw new IllegalArgumentException(args[currentArg] + " " + Launcher.getLocalizedString("missing.arg"));
                } else if ("-verbose".equals(args[currentArg])) {
                    Launcher.setVerbose(true);
                } else {
                    throw new IllegalArgumentException(args[currentArg] + " " + Launcher.getLocalizedString("invalid.arg"));
                }
            }

            // Get target
            String target = null;
            if (currentArg < args.length)
                target = args[currentArg++];
            else
                throw new IllegalArgumentException(Launcher.getLocalizedString("missing.target"));

            // Get user properties 
            for ( ; currentArg < args.length; currentArg++) {
                // If we don't find any more "-" or "-D" arguments, there are no
                // more user properties
                if ("-".equals(args[currentArg])) {
                    currentArg++;
                    break;
                } else if (args[currentArg].length() <= 2 || !"-D".equals(args[currentArg].substring(0, 2))) {
                    break;
                }
                int delimiter = args[currentArg].indexOf('=', 2);
                String key = null;
                String value = null;
                if (delimiter >= 2) {
                    key = args[currentArg].substring(2, delimiter);
                    value = args[currentArg].substring(delimiter + 1);
                } else {
                    // Unfortunately, MS-DOS batch scripts will split an
                    // "-Dname=value" argument into "-Dname" and "value"
                    // arguments. So, we need to assume that the next
                    // argument is the property value unless it appears
                    // to be a different type of argument.
                    key = args[currentArg].substring(2);
                    if (currentArg + 1 < args.length &&
                        !"-D".equals(args[currentArg + 1].substring(0, 2)))
                    {
                        value = args[++currentArg];
                    } else {
                        value = "";
                    }
                }
                project.setUserProperty(key, value);
            }

            // Treat all remaining arguments as application arguments
            String[] appArgs = new String[args.length - currentArg];
            for (int i = 0; i < appArgs.length; i++) {
                appArgs[i] = args[i + currentArg];
                project.setUserProperty(LaunchTask.ARG_PROP_NAME + Integer.toString(i), appArgs[i]);
            }

            // Set standard Ant user properties
            project.setUserProperty("ant.version", Main.getAntVersion());
            project.setUserProperty("ant.file", launchFile.getCanonicalPath());
            project.setUserProperty("ant.java.version", System.getProperty("java.specification.version"));

            // Set the buildfile
            ProjectHelper.configureProject(project, launchFile);

            // Check that the target exists
            if (!project.getTargets().containsKey(target))
                throw new IllegalArgumentException(target + " " + Launcher.getLocalizedString("invalid.target"));

            // Execute the target
            try {
                runtime.addShutdownHook(shutdownHook);
            } catch (NoSuchMethodError nsme) {
                // Early JVMs do not support this method
            }
            project.executeTarget(target);

        } catch (Throwable t) {
            // Log any errors
            returnValue = 1;
            String message = t.getMessage();
            if (t instanceof IllegalArgumentException) {
                Launcher.error(message, true);
            } else {
                if (Launcher.verbose)
                    Launcher.error(t);
                else
                    Launcher.error(message, false);
            }
        } finally {
            synchronized (Launcher.lock) {
                // Remove the shutdown hook
                try {
                    runtime.removeShutdownHook(shutdownHook);
                } catch (NoSuchMethodError nsme) {
                    // Early JVMs do not support this method
                }
                // Reset the class loader after running Ant
                Thread.currentThread().setContextClassLoader(parentLoader);
                // Reset stopped flag
                Launcher.setStarted(false);
                // Notify the stop() method that we have set the class loader
                Launcher.lock.notifyAll();
            }
        }

        // Override return value with exit value of last synchronous child JVM
        Process[] childProcesses = LaunchTask.getChildProcesses();
        if (childProcesses.length > 0)
            returnValue = childProcesses[childProcesses.length - 1].exitValue();

        return returnValue;

    }

    /**
     * Interrupt the {@link #start(String[])} method. This is done
     * by forcing the current or next scheduled invocation of the
     * {@link LaunchTask#execute()} method to throw an exception. In addition,
     * this method will terminate any synchronous child processes that any
     * instances of the {@link LaunchTask} class have launched. Note, however,
     * that this method will <b>not</b> terminate any asynchronous child
     * processes that have been launched. Accordingly, applications that use
     * this method are encouraged to always set the LaunchTask.TASK_NAME task's
     * "waitForChild" attribute to "true" to ensure that the
     * application that you want to control can be terminated via this method.
     * After this method has been executed, it will not return until is safe to
     * execute the {@link #start(String[])} method.
     *
     * @return true if this method completed without error and false if an
     *  error occurred or the launch process is already stopped
     */
    public static boolean stop() {

        synchronized (Launcher.lock) {
            // Check the stopped flag to avoid concurrent execution of this
            // method
            if (Launcher.isStopped())
                return false;

            // Make sure that the start() method is running. If not, just
            // return as there is nothing to do.
            if (Launcher.isStarted())
                Launcher.setStopped(true);
            else
                return false;
        }

        boolean returnValue = true;

        try {

            // Kill all of the synchronous child processes
            killChildProcesses();

            // Wait for the start() method to reset the start flag
            synchronized (Launcher.lock) {
                if (Launcher.isStarted())
                    Launcher.lock.wait();
            }

            // Make sure that the start() method has really finished
            if (Launcher.isStarted())
                returnValue = true;

        } catch (Throwable t) {
            // Log any errors
            returnValue = false;
            String message = t.getMessage();
            if (Launcher.verbose)
                Launcher.error(t);
            else
                Launcher.error(message, false);
        } finally {
            // Reset stopped flag
            Launcher.setStopped(false);
        }

        return returnValue;

    }

    /**
     * Print a detailed error message and exit.
     *
     * @param message the message to be printed
     * @param usage if true, print a usage statement after the message
     */
    public static void error(String message, boolean usage) {

        if (message != null)
            Launcher.getLog().println(Launcher.getLocalizedString("error") + ": " + message);
        if (usage)
            Launcher.getLog().println(Launcher.getLocalizedString("usage"));

    }

    /**
     * Print a detailed error message and exit.
     *
     * @param message the exception whose stack trace is to be printed.
     */
    public static void error(Throwable t) {

        String message = t.getMessage();
        if (!Launcher.verbose && message != null)
            Launcher.getLog().println(Launcher.getLocalizedString("error") + ": " + message);
        else
            t.printStackTrace(Launcher.getLog());

    }

    /**
     * Get the canonical directory of the class or jar file that this class was
     * loaded. This method can be used to calculate the root directory of an
     * installation.
     *
     * @return the canonical directory of the class or jar file that this class
     *  file was loaded from
     * @throws IOException if the canonical directory or jar file
     *  cannot be found
     */
    public static File getBootstrapDir() throws IOException {

        File file = Launcher.getBootstrapFile();
        if (file.isDirectory())
            return file;
        else
            return file.getParentFile();

    }

    /**
     * Get the canonical directory or jar file that this class was loaded
     * from.
     *
     * @return the canonical directory or jar file that this class
     *  file was loaded from
     * @throws IOException if the canonical directory or jar file
     *  cannot be found
     */
    public static File getBootstrapFile() throws IOException {

        if (bootstrapFile == null) {

            // Get a URL for where this class was loaded from
            String classResourceName = "/" + Launcher.class.getName().replace('.', '/') + ".class";
            URL resource = Launcher.class.getResource(classResourceName);
            if (resource == null)
                throw new IOException(Launcher.getLocalizedString("bootstrap.file.not.found") + ": " + Launcher.class.getName());
            String resourcePath = null;
            String embeddedClassName = null;
            boolean isJar = false;
            String protocol = resource.getProtocol();
            if ((protocol != null) &&
                (protocol.indexOf("jar") >= 0)) {
                isJar = true;
            }
            if (isJar) {
                resourcePath = URLDecoder.decode(resource.getFile());
                embeddedClassName = "!" + classResourceName;
            } else {
                resourcePath = URLDecoder.decode(resource.toExternalForm());
                embeddedClassName = classResourceName;
            }
            int sep = resourcePath.lastIndexOf(embeddedClassName);
            if (sep >= 0)
                resourcePath = resourcePath.substring(0, sep);

            // Now that we have a URL, make sure that it is a "file" URL
            // as we need to coerce the URL into a File object
            if (resourcePath.indexOf("file:") == 0)
                resourcePath = resourcePath.substring(5);
            else
                throw new IOException(Launcher.getLocalizedString("bootstrap.file.not.found") + ": " + Launcher.class.getName());

            // Coerce the URL into a file and check that it exists. Note that
            // the JVM <code>File(String)</code> constructor automatically
            // flips all '/' characters to '\' on Windows and there are no
            // valid escape characters so we sould not have to worry about
            // URL encoded slashes.
            File file = new File(resourcePath);
            if (!file.exists() || !file.canRead())
                throw new IOException(Launcher.getLocalizedString("bootstrap.file.not.found") + ": " + Launcher.class.getName());
            bootstrapFile = file.getCanonicalFile();

        }

        return bootstrapFile;

    }

    /**
     * Get the full path of the Java command to execute.
     *
     * @return a string suitable for executing a child JVM
     */
    public static synchronized String getJavaCommand() {

        if (javaCmd == null) {

            String osname = System.getProperty("os.name").toLowerCase();
            String commandName = null;
            if (osname.indexOf("windows") >= 0) {
                // Always use javaw.exe on Windows so that we aren't bound to an
                // MS-DOS window
                commandName = "javaw.exe";
            } else {
                commandName = "java";
            }
            javaCmd = System.getProperty("java.home") + File.separator + "bin" + File.separator + commandName;

        }

        return javaCmd;

    }

    /**
     * Get the full path of the JDB command to execute.
     *
     * @return a string suitable for executing a child JDB debugger
     */
    public static synchronized String getJDBCommand() {

        if (jdbCmd == null) {

            String osname = System.getProperty("os.name").toLowerCase();
            String commandName = null;
            if (osname.indexOf("windows") >= 0)
                commandName = "jdb.exe";
            else
                commandName = "jdb";
            jdbCmd = new File(System.getProperty("java.home")).getParent() + File.separator + "bin" + File.separator + commandName;

        }

        return jdbCmd;

    }

    /**
     * Get the PrintStream that all output should printed to. The default
     * PrintStream returned in System.err.
     *
     * @return the PrintStream instance to print output to
     */
    public static synchronized PrintStream getLog() {

        return Launcher.log;

    }

    /**
     * Set the classpath to the current JVM's tools classes.
     *
     * @return a string suitable for use as a JVM's -classpath argument
     * @throws IOException if the tools classes cannot be found
     */
    public static synchronized String getToolsClasspath() throws IOException {

        if (toolsClasspath == null) {

            File javaHome = null;
            javaHome = new File(System.getProperty("java.home")).getCanonicalFile();
            Class clazz = null;
            String[] toolsPaths = new String[2];
            toolsPaths[0] = javaHome.getParent() + File.separator +
                "lib" + File.separator + "tools.jar";
            toolsPaths[1] = javaHome.getPath() + File.separator +
                "lib" + File.separator + "tools.jar";
            File toolsFile = null;
            for (int i = 0; i < toolsPaths.length; i++) {
                ClassLoader loader = ClassLoader.getSystemClassLoader();
                toolsFile = new File(toolsPaths[i]);
                // Check if the jar file exists and is readable
                if (!toolsFile.isFile() || !toolsFile.canRead())
                    toolsFile = null;
                if (toolsFile != null) {
                    try {
                        URL toolsURL = toolsFile.toURL();
                        loader = new URLClassLoader(new URL[]{toolsURL}, loader);
                    } catch (Exception e) {
                        toolsFile = null;
                    }
                }
                // Try to load the javac class just to be sure. Note that we
                // use the system class loader if the file does not exist to
                // handle cases like Mac OS X where the tools.jar classes are
                // loaded by the bootstrap class loader.
                try {
                    clazz = loader.loadClass("sun.tools.javac.Main");
                    if (clazz != null)
                        break;
                } catch (Exception e) {}
            }

            if (clazz == null)
                throw new IOException(Launcher.getLocalizedString("sdk.tools.not.found"));

            // Save classpath.
            if (toolsFile != null)
                toolsClasspath = toolsFile.getPath();
            else
                toolsClasspath = "";

        }

        return toolsClasspath;

    }

    /**
     * Get a localized property. This method will search for localized
     * properties and will resolve ${...} style macros in the localized string.
     *
     * @param key the localized property to retrieve
     * @return the localized and resolved property value
     */
    public static String getLocalizedString(String key) {

        return Launcher.getLocalizedString(key, Launcher.class.getName());

    }

    /**
     * Get a localized property. This method will search for localized
     * properties and will resolve ${...} style macros in the localized string.
     *
     * @param key the localized property to retrieve
     * @param className the name of the class to retrieve the property for
     * @return the localized and resolved property value
     */
    public static String getLocalizedString(String key, String className) {

        try {
            ResourceBundle resourceBundle = ResourceBundle.getBundle(className);
            return Launcher.resolveString(resourceBundle.getString(key));
        } catch (Exception e) {
            // We should at least make it clear that the property is not
            // defined in the properties file 
            return "<" + key + " property>";
        }

    }

    /**
     * Resolve ${...} style macros in strings. This method will replace any
     * embedded ${...} strings in the specified unresolved parameter with the
     * value of the system property in the enclosed braces. Note that any '$'
     * characters can be escaped by putting '$$' in the specified parameter.
     * In additional, the following special macros will be resolved:
     * <ul>
     * <li><code>${launcher.executable.name}</code> will be substituted with the
     * value of the "org.apache.commons.launcher.executableName" system
     * property, the "-executablename" command line argument, or, if both of
     * those are undefined, with the absolute path to the Java executable plus
     * its classpath and main class name arguments
     * <li><code>${launcher.bootstrap.file}</code> will get substituted with
     * the value returned by {@link #getBootstrapFile()}
     * <li><code>${launcher.bootstrap.dir}</code> will get substituted with
     * the value returned by {@link #getBootstrapDir()}
     *
     * @param unresolved the string to be resolved
     * @return the resolved String
     * @throws IOException if any error occurs
     */
    private static String resolveString(String unresolved) throws IOException {

        if (unresolved == null)
            return null;

        // Substitute system property strings
        StringBuffer buf = new StringBuffer();
        int tokenEnd = 0;
        int tokenStart = 0;
        char token = '$';
        boolean escapeChar = false;
        boolean firstToken = true;
        boolean lastToken = false;

        while (!lastToken) {

            tokenEnd = unresolved.indexOf(token, tokenStart);

            // Determine if this is the first token
            if (firstToken) {
                firstToken = false;
                // Skip if first token is zero length
                if (tokenEnd - tokenStart == 0) {
                    tokenStart = ++tokenEnd;
                    continue;
                }
            }
            // Determine if this is the last token
            if (tokenEnd < 0) {
                lastToken = true;
                tokenEnd = unresolved.length();
            }

            if (escapeChar) {

                // Don't parse the string
                buf.append(token + unresolved.substring(tokenStart, tokenEnd));
                escapeChar = !escapeChar;

            } else {

                // Parse the string
                int openProp = unresolved.indexOf('{', tokenStart);
                int closeProp = unresolved.indexOf('}', tokenStart + 1);
                String prop = null;

                // We must have a '{' in the first character and a closing
                // '}' after that
                if (openProp != tokenStart ||
                    closeProp < tokenStart + 1 ||
                    closeProp >= tokenEnd)
                {
                    buf.append(unresolved.substring(tokenStart, tokenEnd));
                } else {
                    // Property found
                    String propName = unresolved.substring(tokenStart + 1, closeProp);
                    if ("launcher.executable.name".equals(propName)) {
                        prop = System.getProperty(ChildMain.EXECUTABLE_PROP_NAME);
                        if (prop != null) {
                            // Quote the property
                            prop = "\"" + prop + "\"";
                        } else {
                            // Set property to fully quoted Java command line
                            String classpath = Launcher.getBootstrapFile().getPath();
                            prop = "\"" + System.getProperty("java.home") + File.separator + "bin" + File.separator + "java\" -classpath \"" + classpath + "\" LauncherBootstrap";
                        }
                    } else if ("launcher.bootstrap.file".equals(propName)) {
                        prop = Launcher.getBootstrapFile().getPath();
                    } else if ("launcher.bootstrap.dir".equals(propName)) {
                        prop = Launcher.getBootstrapDir().getPath();
                    } else {
                        prop = System.getProperty(unresolved.substring(tokenStart + 1, closeProp));
                    }
                    if (prop == null)
                        prop = "";
                    buf.append(prop + unresolved.substring(++closeProp, tokenEnd));
                }

            }

            // If this is a blank token, then the next starts with the
            // token character. So, treat this token as an escape
            // character for the next token.
            if (tokenEnd - tokenStart == 0)
                escapeChar = !escapeChar;

            tokenStart = ++tokenEnd;

        }

        return buf.toString();

    }

    /**
     * Set the PrintStream that all output should printed to.
     *
     * @param a PrintStream instance to print output to
     */
    public static synchronized void setLog(PrintStream log) {

        if (log != null)
            Launcher.log = log;
        else
            Launcher.log = System.err;

    }

    /**
     * Set the started flag.
     *
     * @param started the value of the started flag
     */
    private static synchronized void setStarted(boolean started) {

        Launcher.started = started;

    }

    /**
     * Set the stopped flag.
     *
     * @param stopped the value of the stopped flag
     */
    private static synchronized void setStopped(boolean stopped) {

        Launcher.stopped = stopped;

    }

    /**
     * Set the verbose flag.
     *
     * @param verbose the value of the verbose flag
     */
    public static synchronized void setVerbose(boolean verbose) {

        Launcher.verbose = verbose;

    }

    /**
     * Iterate through the list of synchronous child process launched by
     * all of the {@link LaunchTask} instances.
     */
    public static void killChildProcesses() {

        Process[] procs = LaunchTask.getChildProcesses();
        for (int i = 0; i < procs.length; i++)
            procs[i].destroy();

    }

    //----------------------------------------------------------------- Methods

    /**
     * Wrapper to allow the {@link #killChildProcesses()} method to be
     * invoked in a shutdown hook.
     */
    public void run() {

        Launcher.killChildProcesses();

    }

}
