001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 * 
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 * 
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.launcher;
019
020import java.io.File;
021import java.io.IOException;
022import java.io.PrintStream;
023import java.net.URL;
024import java.net.URLClassLoader;
025import java.net.URLDecoder;
026import java.util.ResourceBundle;
027
028import org.apache.commons.launcher.types.ArgumentSet;
029import org.apache.commons.launcher.types.JVMArgumentSet;
030import org.apache.commons.launcher.types.SysPropertySet;
031import org.apache.tools.ant.Main;
032import org.apache.tools.ant.Project;
033import org.apache.tools.ant.ProjectHelper;
034import org.apache.tools.ant.taskdefs.Ant;
035import org.apache.tools.ant.taskdefs.Available;
036import org.apache.tools.ant.taskdefs.CallTarget;
037import org.apache.tools.ant.taskdefs.ConditionTask;
038import org.apache.tools.ant.taskdefs.ExecTask;
039import org.apache.tools.ant.taskdefs.Exit;
040import org.apache.tools.ant.taskdefs.Property;
041import org.apache.tools.ant.taskdefs.Mkdir;
042import org.apache.tools.ant.taskdefs.Copy;
043import org.apache.tools.ant.taskdefs.Delete;
044import org.apache.tools.ant.taskdefs.Taskdef;
045import org.apache.tools.ant.taskdefs.WaitFor;
046import org.apache.tools.ant.types.Description;
047import org.apache.tools.ant.types.FileList;
048import org.apache.tools.ant.types.FileSet;
049import org.apache.tools.ant.types.Path;
050import org.apache.tools.ant.types.PatternSet;
051
052/**
053 * A class that is used to launch a Java process. The primary purpose of this
054 * class is to eliminate the need for a batch or shell script to launch a Java
055 * process. Some situations where elimination of a batch or shell script may be 
056 * desirable are:
057 * <ul>
058 * <li>You want to avoid having to determining where certain application paths
059 *  are e.g. your application's home directory, etc. Determining this
060 *  dynamically in a Windows batch scripts is very tricky on some versions of
061 *  Windows or when softlinks are used on Unix platforms.
062 * <li>You need to enforce certain properties e.g. java.endorsed.dirs when
063 *  running with JDK 1.4.
064 * <li>You want to allow users to pass in custom JVM arguments or system
065 *  properties without having to parse and reorder arguments in your script.
066 *  This can be tricky and/or messy in batch and shell scripts.
067 * <li>You want to bootstrap Java properties from a configuration file instead
068 *  hard-coding them in your batch and shell scripts.
069 * <li>You want to provide localized error messages which is very tricky to do
070 *  in batch and shell scripts.
071 * </ul>
072 *
073 * @author Patrick Luby
074 */
075public class Launcher implements Runnable {
076
077    //----------------------------------------------------------- Static Fields
078
079
080    /**
081     * Cached bootstrap file.
082     */
083    private static File bootstrapFile = null;
084
085    /**
086     * Cached java command
087     */
088    private static String javaCmd = null;
089
090    /**
091     * Cached JDB command
092     */
093    private static String jdbCmd = null;
094
095    /**
096     * Default XML file name
097     */
098    private final static String DEFAULT_XML_FILE_NAME = "launcher.xml";
099
100    /**
101     * Shared lock.
102     */
103    private static Object lock = new Object();
104
105    /**
106     * Cached log
107     */
108    private static PrintStream log = System.err;
109
110    /**
111     * Cached resourceBundle
112     */
113    private static ResourceBundle resourceBundle = null;
114
115    /**
116     * The started status flag.
117     */
118    private static boolean started = false;
119
120    /**
121     * The stopped status flag.
122     */
123    private static boolean stopped = false;
124
125    /**
126     * List of supported Ant tasks.
127     */
128    public final static Object[] SUPPORTED_ANT_TASKS = new Object[] {
129            LaunchTask.TASK_NAME, LaunchTask.class,
130            "ant", Ant.class,
131            "antcall", CallTarget.class,
132            "available", Available.class,
133            "condition", ConditionTask.class,
134            "fail", Exit.class,
135            "property", Property.class,
136            "mkdir", Mkdir.class,
137            "delete", Delete.class,
138            "copy", Copy.class,
139            "exec", ExecTask.class,
140            "waitfor", WaitFor.class,
141            "taskdef", Taskdef.class
142        };
143
144    /**
145     * List of supported Ant types.
146     */
147    public final static Object[] SUPPORTED_ANT_TYPES = new Object[] {
148            ArgumentSet.TYPE_NAME, ArgumentSet.class,
149            JVMArgumentSet.TYPE_NAME, JVMArgumentSet.class,
150            SysPropertySet.TYPE_NAME, SysPropertySet.class,
151            "description", Description.class,
152            "fileset", FileSet.class,
153            "filelist", FileList.class,
154            "path", Path.class,
155            "patternset", PatternSet.class
156        };
157
158    /**
159     * Cached tools classpath.
160     */
161    private static String toolsClasspath = null;
162
163    /**
164     * The verbose flag
165     */
166    private static boolean verbose = false;
167
168    //---------------------------------------------------------- Static Methods
169
170
171    /**
172     * Get the started flag.
173     *
174     * @return the value of the started flag
175     */
176    public static synchronized boolean isStarted() {
177
178        return Launcher.started;
179
180    }
181
182    /**
183     * Get the stopped flag.
184     *
185     * @return the value of the stopped flag
186     */
187    public static synchronized boolean isStopped() {
188
189        return Launcher.stopped;
190
191    }
192
193    /**
194     * Start the launching process. This method is essential the
195     * <code>main(String[])<code> method for this class except that this method
196     * never invokes {@link System#exit(int)}. This method is designed for
197     * applications that wish to invoke this class directly from within their
198     * application's code.
199     *
200     * @param args command line arguments
201     * @return the exit value of the last synchronous child JVM that was
202     *  launched or 1 if any other error occurs
203     * @throws IllegalArgumentException if any error parsing the args parameter
204     *  occurs
205     */
206    public static int start(String[] args) throws IllegalArgumentException {
207
208        // Check make sure that neither this method or the stop() method is
209        // already running since we do not support concurrency
210        synchronized (Launcher.lock) {
211            if (Launcher.isStarted() || Launcher.isStopped())
212                return 1;
213            Launcher.setStarted(true);
214        }
215
216        int returnValue = 0;
217        ClassLoader parentLoader = null;
218        Thread shutdownHook = new Thread(new Launcher());
219        Runtime runtime = Runtime.getRuntime();
220
221        try {
222
223            // Cache the current class loader for this thread and set the class
224            // loader before running Ant. Note that we only set the class loader
225            // if we are running a Java version earlier than 1.4 as on 1.4 this
226            // causes unnecessary loading of the XML parser classes.
227            parentLoader = Thread.currentThread().getContextClassLoader();
228            boolean lessThan14 = true;
229            try {
230                Class.forName("java.lang.CharSequence");
231                lessThan14 = false;
232            } catch (ClassNotFoundException cnfe) {
233                // If this class does not exist, then we are not running Java 1.4
234            }
235            if (lessThan14)
236                Thread.currentThread().setContextClassLoader(Launcher.class.getClassLoader());
237
238            Project project = new Project();
239
240            // Set the project's class loader
241            project.setCoreLoader(Launcher.class.getClassLoader());
242
243            // Initialize the project. Note that we don't invoke the
244            // Project.init() method directly as this will cause all of
245            // the myriad of Task subclasses to load which is a big
246            // performance hit. Instead, we load only the
247            // Launcher.SUPPORTED_ANT_TASKS and Launcher.SUPPORTED_ANT_TYPES
248            // into the project that the Launcher supports.
249            for (int i = 0; i < Launcher.SUPPORTED_ANT_TASKS.length; i++) {
250                // The even numbered elements should be the task name
251                String taskName = (String)Launcher.SUPPORTED_ANT_TASKS[i];
252                // The odd numbered elements should be the task class
253                Class taskClass = (Class)Launcher.SUPPORTED_ANT_TASKS[++i];
254                project.addTaskDefinition(taskName, taskClass);
255            }
256            for (int i = 0; i < Launcher.SUPPORTED_ANT_TYPES.length; i++) {
257                // The even numbered elements should be the type name
258                String typeName = (String)Launcher.SUPPORTED_ANT_TYPES[i];
259                // The odd numbered elements should be the type class
260                Class typeClass = (Class)Launcher.SUPPORTED_ANT_TYPES[++i];
261                project.addDataTypeDefinition(typeName, typeClass);
262            }
263
264            // Add all system properties as project properties
265            project.setSystemProperties();
266
267            // Parse the arguments
268            int currentArg = 0;
269
270            // Set default XML file
271            File launchFile = new File(Launcher.getBootstrapDir(), Launcher.DEFAULT_XML_FILE_NAME);
272
273            // Get standard launcher arguments
274            for ( ; currentArg < args.length; currentArg++) {
275                // If we find a "-" argument or an argument without a
276                // leading "-", there are no more standard launcher arguments
277                if ("-".equals(args[currentArg])) {
278                    currentArg++;
279                    break;
280                } else if (args[currentArg].length() > 0 && !"-".equals(args[currentArg].substring(0, 1))) {
281                    break;
282                } else if ("-help".equals(args[currentArg])) {
283                    throw new IllegalArgumentException();
284                } else if ("-launchfile".equals(args[currentArg])) {
285                    if (currentArg + 1 < args.length){
286                        String fileArg = args[++currentArg];
287                        launchFile = new File(fileArg);
288                        if (!launchFile.isAbsolute())
289                            launchFile = new File(Launcher.getBootstrapDir(), fileArg);
290                    } else {
291                        throw new IllegalArgumentException(args[currentArg] + " " + Launcher.getLocalizedString("missing.arg"));
292                    }
293                } else if ("-executablename".equals(args[currentArg])) {
294                    if (currentArg + 1 < args.length)
295                        System.setProperty(ChildMain.EXECUTABLE_PROP_NAME, args[++currentArg]);
296                    else
297                        throw new IllegalArgumentException(args[currentArg] + " " + Launcher.getLocalizedString("missing.arg"));
298                } else if ("-verbose".equals(args[currentArg])) {
299                    Launcher.setVerbose(true);
300                } else {
301                    throw new IllegalArgumentException(args[currentArg] + " " + Launcher.getLocalizedString("invalid.arg"));
302                }
303            }
304
305            // Get target
306            String target = null;
307            if (currentArg < args.length)
308                target = args[currentArg++];
309            else
310                throw new IllegalArgumentException(Launcher.getLocalizedString("missing.target"));
311
312            // Get user properties 
313            for ( ; currentArg < args.length; currentArg++) {
314                // If we don't find any more "-" or "-D" arguments, there are no
315                // more user properties
316                if ("-".equals(args[currentArg])) {
317                    currentArg++;
318                    break;
319                } else if (args[currentArg].length() <= 2 || !"-D".equals(args[currentArg].substring(0, 2))) {
320                    break;
321                }
322                int delimiter = args[currentArg].indexOf('=', 2);
323                String key = null;
324                String value = null;
325                if (delimiter >= 2) {
326                    key = args[currentArg].substring(2, delimiter);
327                    value = args[currentArg].substring(delimiter + 1);
328                } else {
329                    // Unfortunately, MS-DOS batch scripts will split an
330                    // "-Dname=value" argument into "-Dname" and "value"
331                    // arguments. So, we need to assume that the next
332                    // argument is the property value unless it appears
333                    // to be a different type of argument.
334                    key = args[currentArg].substring(2);
335                    if (currentArg + 1 < args.length &&
336                        !"-D".equals(args[currentArg + 1].substring(0, 2)))
337                    {
338                        value = args[++currentArg];
339                    } else {
340                        value = "";
341                    }
342                }
343                project.setUserProperty(key, value);
344            }
345
346            // Treat all remaining arguments as application arguments
347            String[] appArgs = new String[args.length - currentArg];
348            for (int i = 0; i < appArgs.length; i++) {
349                appArgs[i] = args[i + currentArg];
350                project.setUserProperty(LaunchTask.ARG_PROP_NAME + Integer.toString(i), appArgs[i]);
351            }
352
353            // Set standard Ant user properties
354            project.setUserProperty("ant.version", Main.getAntVersion());
355            project.setUserProperty("ant.file", launchFile.getCanonicalPath());
356            project.setUserProperty("ant.java.version", System.getProperty("java.specification.version"));
357
358            // Set the buildfile
359            ProjectHelper.configureProject(project, launchFile);
360
361            // Check that the target exists
362            if (!project.getTargets().containsKey(target))
363                throw new IllegalArgumentException(target + " " + Launcher.getLocalizedString("invalid.target"));
364
365            // Execute the target
366            try {
367                runtime.addShutdownHook(shutdownHook);
368            } catch (NoSuchMethodError nsme) {
369                // Early JVMs do not support this method
370            }
371            project.executeTarget(target);
372
373        } catch (Throwable t) {
374            // Log any errors
375            returnValue = 1;
376            String message = t.getMessage();
377            if (t instanceof IllegalArgumentException) {
378                Launcher.error(message, true);
379            } else {
380                if (Launcher.verbose)
381                    Launcher.error(t);
382                else
383                    Launcher.error(message, false);
384            }
385        } finally {
386            synchronized (Launcher.lock) {
387                // Remove the shutdown hook
388                try {
389                    runtime.removeShutdownHook(shutdownHook);
390                } catch (NoSuchMethodError nsme) {
391                    // Early JVMs do not support this method
392                }
393                // Reset the class loader after running Ant
394                Thread.currentThread().setContextClassLoader(parentLoader);
395                // Reset stopped flag
396                Launcher.setStarted(false);
397                // Notify the stop() method that we have set the class loader
398                Launcher.lock.notifyAll();
399            }
400        }
401
402        // Override return value with exit value of last synchronous child JVM
403        Process[] childProcesses = LaunchTask.getChildProcesses();
404        if (childProcesses.length > 0)
405            returnValue = childProcesses[childProcesses.length - 1].exitValue();
406
407        return returnValue;
408
409    }
410
411    /**
412     * Interrupt the {@link #start(String[])} method. This is done
413     * by forcing the current or next scheduled invocation of the
414     * {@link LaunchTask#execute()} method to throw an exception. In addition,
415     * this method will terminate any synchronous child processes that any
416     * instances of the {@link LaunchTask} class have launched. Note, however,
417     * that this method will <b>not</b> terminate any asynchronous child
418     * processes that have been launched. Accordingly, applications that use
419     * this method are encouraged to always set the LaunchTask.TASK_NAME task's
420     * "waitForChild" attribute to "true" to ensure that the
421     * application that you want to control can be terminated via this method.
422     * After this method has been executed, it will not return until is safe to
423     * execute the {@link #start(String[])} method.
424     *
425     * @return true if this method completed without error and false if an
426     *  error occurred or the launch process is already stopped
427     */
428    public static boolean stop() {
429
430        synchronized (Launcher.lock) {
431            // Check the stopped flag to avoid concurrent execution of this
432            // method
433            if (Launcher.isStopped())
434                return false;
435
436            // Make sure that the start() method is running. If not, just
437            // return as there is nothing to do.
438            if (Launcher.isStarted())
439                Launcher.setStopped(true);
440            else
441                return false;
442        }
443
444        boolean returnValue = true;
445
446        try {
447
448            // Kill all of the synchronous child processes
449            killChildProcesses();
450
451            // Wait for the start() method to reset the start flag
452            synchronized (Launcher.lock) {
453                if (Launcher.isStarted())
454                    Launcher.lock.wait();
455            }
456
457            // Make sure that the start() method has really finished
458            if (Launcher.isStarted())
459                returnValue = true;
460
461        } catch (Throwable t) {
462            // Log any errors
463            returnValue = false;
464            String message = t.getMessage();
465            if (Launcher.verbose)
466                Launcher.error(t);
467            else
468                Launcher.error(message, false);
469        } finally {
470            // Reset stopped flag
471            Launcher.setStopped(false);
472        }
473
474        return returnValue;
475
476    }
477
478    /**
479     * Print a detailed error message and exit.
480     *
481     * @param message the message to be printed
482     * @param usage if true, print a usage statement after the message
483     */
484    public static void error(String message, boolean usage) {
485
486        if (message != null)
487            Launcher.getLog().println(Launcher.getLocalizedString("error") + ": " + message);
488        if (usage)
489            Launcher.getLog().println(Launcher.getLocalizedString("usage"));
490
491    }
492
493    /**
494     * Print a detailed error message and exit.
495     *
496     * @param t the exception whose stack trace is to be printed.
497     */
498    public static void error(Throwable t) {
499
500        String message = t.getMessage();
501        if (!Launcher.verbose && message != null)
502            Launcher.getLog().println(Launcher.getLocalizedString("error") + ": " + message);
503        else
504            t.printStackTrace(Launcher.getLog());
505
506    }
507
508    /**
509     * Get the canonical directory of the class or jar file that this class was
510     * loaded. This method can be used to calculate the root directory of an
511     * installation.
512     *
513     * @return the canonical directory of the class or jar file that this class
514     *  file was loaded from
515     * @throws IOException if the canonical directory or jar file
516     *  cannot be found
517     */
518    public static File getBootstrapDir() throws IOException {
519
520        File file = Launcher.getBootstrapFile();
521        if (file.isDirectory())
522            return file;
523        else
524            return file.getParentFile();
525
526    }
527
528    /**
529     * Get the canonical directory or jar file that this class was loaded
530     * from.
531     *
532     * @return the canonical directory or jar file that this class
533     *  file was loaded from
534     * @throws IOException if the canonical directory or jar file
535     *  cannot be found
536     */
537    public static File getBootstrapFile() throws IOException {
538
539        if (bootstrapFile == null) {
540
541            // Get a URL for where this class was loaded from
542            String classResourceName = "/" + Launcher.class.getName().replace('.', '/') + ".class";
543            URL resource = Launcher.class.getResource(classResourceName);
544            if (resource == null)
545                throw new IOException(Launcher.getLocalizedString("bootstrap.file.not.found") + ": " + Launcher.class.getName());
546            String resourcePath = null;
547            String embeddedClassName = null;
548            boolean isJar = false;
549            String protocol = resource.getProtocol();
550            if ((protocol != null) &&
551                (protocol.indexOf("jar") >= 0)) {
552                isJar = true;
553            }
554            if (isJar) {
555                resourcePath = URLDecoder.decode(resource.getFile());
556                embeddedClassName = "!" + classResourceName;
557            } else {
558                resourcePath = URLDecoder.decode(resource.toExternalForm());
559                embeddedClassName = classResourceName;
560            }
561            int sep = resourcePath.lastIndexOf(embeddedClassName);
562            if (sep >= 0)
563                resourcePath = resourcePath.substring(0, sep);
564
565            // Now that we have a URL, make sure that it is a "file" URL
566            // as we need to coerce the URL into a File object
567            if (resourcePath.indexOf("file:") == 0)
568                resourcePath = resourcePath.substring(5);
569            else
570                throw new IOException(Launcher.getLocalizedString("bootstrap.file.not.found") + ": " + Launcher.class.getName());
571
572            // Coerce the URL into a file and check that it exists. Note that
573            // the JVM <code>File(String)</code> constructor automatically
574            // flips all '/' characters to '\' on Windows and there are no
575            // valid escape characters so we sould not have to worry about
576            // URL encoded slashes.
577            File file = new File(resourcePath);
578            if (!file.exists() || !file.canRead())
579                throw new IOException(Launcher.getLocalizedString("bootstrap.file.not.found") + ": " + Launcher.class.getName());
580            bootstrapFile = file.getCanonicalFile();
581
582        }
583
584        return bootstrapFile;
585
586    }
587
588    /**
589     * Get the full path of the Java command to execute.
590     *
591     * @return a string suitable for executing a child JVM
592     */
593    public static synchronized String getJavaCommand() {
594
595        if (javaCmd == null) {
596
597            String osname = System.getProperty("os.name").toLowerCase();
598            String commandName = null;
599            if (osname.indexOf("windows") >= 0) {
600                // Always use javaw.exe on Windows so that we aren't bound to an
601                // MS-DOS window
602                commandName = "javaw.exe";
603            } else {
604                commandName = "java";
605            }
606            javaCmd = System.getProperty("java.home") + File.separator + "bin" + File.separator + commandName;
607
608        }
609
610        return javaCmd;
611
612    }
613
614    /**
615     * Get the full path of the JDB command to execute.
616     *
617     * @return a string suitable for executing a child JDB debugger
618     */
619    public static synchronized String getJDBCommand() {
620
621        if (jdbCmd == null) {
622
623            String osname = System.getProperty("os.name").toLowerCase();
624            String commandName = null;
625            if (osname.indexOf("windows") >= 0)
626                commandName = "jdb.exe";
627            else
628                commandName = "jdb";
629            jdbCmd = new File(System.getProperty("java.home")).getParent() + File.separator + "bin" + File.separator + commandName;
630
631        }
632
633        return jdbCmd;
634
635    }
636
637    /**
638     * Get the PrintStream that all output should printed to. The default
639     * PrintStream returned in System.err.
640     *
641     * @return the PrintStream instance to print output to
642     */
643    public static synchronized PrintStream getLog() {
644
645        return Launcher.log;
646
647    }
648
649    /**
650     * Set the classpath to the current JVM's tools classes.
651     *
652     * @return a string suitable for use as a JVM's -classpath argument
653     * @throws IOException if the tools classes cannot be found
654     */
655    public static synchronized String getToolsClasspath() throws IOException {
656
657        if (toolsClasspath == null) {
658
659            File javaHome = null;
660            javaHome = new File(System.getProperty("java.home")).getCanonicalFile();
661            Class clazz = null;
662            String[] toolsPaths = new String[2];
663            toolsPaths[0] = javaHome.getParent() + File.separator +
664                "lib" + File.separator + "tools.jar";
665            toolsPaths[1] = javaHome.getPath() + File.separator +
666                "lib" + File.separator + "tools.jar";
667            File toolsFile = null;
668            for (int i = 0; i < toolsPaths.length; i++) {
669                ClassLoader loader = ClassLoader.getSystemClassLoader();
670                toolsFile = new File(toolsPaths[i]);
671                // Check if the jar file exists and is readable
672                if (!toolsFile.isFile() || !toolsFile.canRead())
673                    toolsFile = null;
674                if (toolsFile != null) {
675                    try {
676                        URL toolsURL = toolsFile.toURL();
677                        loader = new URLClassLoader(new URL[]{toolsURL}, loader);
678                    } catch (Exception e) {
679                        toolsFile = null;
680                    }
681                }
682                // Try to load the javac class just to be sure. Note that we
683                // use the system class loader if the file does not exist to
684                // handle cases like Mac OS X where the tools.jar classes are
685                // loaded by the bootstrap class loader.
686                try {
687                    clazz = loader.loadClass("sun.tools.javac.Main");
688                    if (clazz != null)
689                        break;
690                } catch (Exception e) {}
691            }
692
693            if (clazz == null)
694                throw new IOException(Launcher.getLocalizedString("sdk.tools.not.found"));
695
696            // Save classpath.
697            if (toolsFile != null)
698                toolsClasspath = toolsFile.getPath();
699            else
700                toolsClasspath = "";
701
702        }
703
704        return toolsClasspath;
705
706    }
707
708    /**
709     * Get a localized property. This method will search for localized
710     * properties and will resolve ${...} style macros in the localized string.
711     *
712     * @param key the localized property to retrieve
713     * @return the localized and resolved property value
714     */
715    public static String getLocalizedString(String key) {
716
717        return Launcher.getLocalizedString(key, Launcher.class.getName());
718
719    }
720
721    /**
722     * Get a localized property. This method will search for localized
723     * properties and will resolve ${...} style macros in the localized string.
724     *
725     * @param key the localized property to retrieve
726     * @param className the name of the class to retrieve the property for
727     * @return the localized and resolved property value
728     */
729    public static String getLocalizedString(String key, String className) {
730
731        try {
732            ResourceBundle resourceBundle = ResourceBundle.getBundle(className);
733            return Launcher.resolveString(resourceBundle.getString(key));
734        } catch (Exception e) {
735            // We should at least make it clear that the property is not
736            // defined in the properties file 
737            return "<" + key + " property>";
738        }
739
740    }
741
742    /**
743     * Resolve ${...} style macros in strings. This method will replace any
744     * embedded ${...} strings in the specified unresolved parameter with the
745     * value of the system property in the enclosed braces. Note that any '$'
746     * characters can be escaped by putting '$$' in the specified parameter.
747     * In additional, the following special macros will be resolved:
748     * <ul>
749     * <li><code>${launcher.executable.name}</code> will be substituted with the
750     * value of the "org.apache.commons.launcher.executableName" system
751     * property, the "-executablename" command line argument, or, if both of
752     * those are undefined, with the absolute path to the Java executable plus
753     * its classpath and main class name arguments
754     * <li><code>${launcher.bootstrap.file}</code> will get substituted with
755     * the value returned by {@link #getBootstrapFile()}
756     * <li><code>${launcher.bootstrap.dir}</code> will get substituted with
757     * the value returned by {@link #getBootstrapDir()}
758     *
759     * @param unresolved the string to be resolved
760     * @return the resolved String
761     * @throws IOException if any error occurs
762     */
763    private static String resolveString(String unresolved) throws IOException {
764
765        if (unresolved == null)
766            return null;
767
768        // Substitute system property strings
769        StringBuffer buf = new StringBuffer();
770        int tokenEnd = 0;
771        int tokenStart = 0;
772        char token = '$';
773        boolean escapeChar = false;
774        boolean firstToken = true;
775        boolean lastToken = false;
776
777        while (!lastToken) {
778
779            tokenEnd = unresolved.indexOf(token, tokenStart);
780
781            // Determine if this is the first token
782            if (firstToken) {
783                firstToken = false;
784                // Skip if first token is zero length
785                if (tokenEnd - tokenStart == 0) {
786                    tokenStart = ++tokenEnd;
787                    continue;
788                }
789            }
790            // Determine if this is the last token
791            if (tokenEnd < 0) {
792                lastToken = true;
793                tokenEnd = unresolved.length();
794            }
795
796            if (escapeChar) {
797
798                // Don't parse the string
799                buf.append(token + unresolved.substring(tokenStart, tokenEnd));
800                escapeChar = !escapeChar;
801
802            } else {
803
804                // Parse the string
805                int openProp = unresolved.indexOf('{', tokenStart);
806                int closeProp = unresolved.indexOf('}', tokenStart + 1);
807                String prop = null;
808
809                // We must have a '{' in the first character and a closing
810                // '}' after that
811                if (openProp != tokenStart ||
812                    closeProp < tokenStart + 1 ||
813                    closeProp >= tokenEnd)
814                {
815                    buf.append(unresolved.substring(tokenStart, tokenEnd));
816                } else {
817                    // Property found
818                    String propName = unresolved.substring(tokenStart + 1, closeProp);
819                    if ("launcher.executable.name".equals(propName)) {
820                        prop = System.getProperty(ChildMain.EXECUTABLE_PROP_NAME);
821                        if (prop != null) {
822                            // Quote the property
823                            prop = "\"" + prop + "\"";
824                        } else {
825                            // Set property to fully quoted Java command line
826                            String classpath = Launcher.getBootstrapFile().getPath();
827                            prop = "\"" + System.getProperty("java.home") + File.separator + "bin" + File.separator + "java\" -classpath \"" + classpath + "\" LauncherBootstrap";
828                        }
829                    } else if ("launcher.bootstrap.file".equals(propName)) {
830                        prop = Launcher.getBootstrapFile().getPath();
831                    } else if ("launcher.bootstrap.dir".equals(propName)) {
832                        prop = Launcher.getBootstrapDir().getPath();
833                    } else {
834                        prop = System.getProperty(unresolved.substring(tokenStart + 1, closeProp));
835                    }
836                    if (prop == null)
837                        prop = "";
838                    buf.append(prop + unresolved.substring(++closeProp, tokenEnd));
839                }
840
841            }
842
843            // If this is a blank token, then the next starts with the
844            // token character. So, treat this token as an escape
845            // character for the next token.
846            if (tokenEnd - tokenStart == 0)
847                escapeChar = !escapeChar;
848
849            tokenStart = ++tokenEnd;
850
851        }
852
853        return buf.toString();
854
855    }
856
857    /**
858     * Set the PrintStream that all output should printed to.
859     *
860     * @param log PrintStream instance to print output to
861     */
862    public static synchronized void setLog(PrintStream log) {
863
864        if (log != null)
865            Launcher.log = log;
866        else
867            Launcher.log = System.err;
868
869    }
870
871    /**
872     * Set the started flag.
873     *
874     * @param started the value of the started flag
875     */
876    private static synchronized void setStarted(boolean started) {
877
878        Launcher.started = started;
879
880    }
881
882    /**
883     * Set the stopped flag.
884     *
885     * @param stopped the value of the stopped flag
886     */
887    private static synchronized void setStopped(boolean stopped) {
888
889        Launcher.stopped = stopped;
890
891    }
892
893    /**
894     * Set the verbose flag.
895     *
896     * @param verbose the value of the verbose flag
897     */
898    public static synchronized void setVerbose(boolean verbose) {
899
900        Launcher.verbose = verbose;
901
902    }
903
904    /**
905     * Iterate through the list of synchronous child process launched by
906     * all of the {@link LaunchTask} instances.
907     */
908    public static void killChildProcesses() {
909
910        Process[] procs = LaunchTask.getChildProcesses();
911        for (int i = 0; i < procs.length; i++)
912            procs[i].destroy();
913
914    }
915
916    //----------------------------------------------------------------- Methods
917
918    /**
919     * Wrapper to allow the {@link #killChildProcesses()} method to be
920     * invoked in a shutdown hook.
921     */
922    public void run() {
923
924        Launcher.killChildProcesses();
925
926    }
927
928}