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    
018    package org.apache.commons.launcher;
019    
020    import java.io.File;
021    import java.io.IOException;
022    import java.io.PrintStream;
023    import java.net.URL;
024    import java.net.URLClassLoader;
025    import java.net.URLDecoder;
026    import java.util.ResourceBundle;
027    
028    import org.apache.commons.launcher.types.ArgumentSet;
029    import org.apache.commons.launcher.types.JVMArgumentSet;
030    import org.apache.commons.launcher.types.SysPropertySet;
031    import org.apache.tools.ant.Main;
032    import org.apache.tools.ant.Project;
033    import org.apache.tools.ant.ProjectHelper;
034    import org.apache.tools.ant.taskdefs.Ant;
035    import org.apache.tools.ant.taskdefs.Available;
036    import org.apache.tools.ant.taskdefs.CallTarget;
037    import org.apache.tools.ant.taskdefs.ConditionTask;
038    import org.apache.tools.ant.taskdefs.ExecTask;
039    import org.apache.tools.ant.taskdefs.Exit;
040    import org.apache.tools.ant.taskdefs.Property;
041    import org.apache.tools.ant.taskdefs.Mkdir;
042    import org.apache.tools.ant.taskdefs.Copy;
043    import org.apache.tools.ant.taskdefs.Delete;
044    import org.apache.tools.ant.taskdefs.Taskdef;
045    import org.apache.tools.ant.taskdefs.WaitFor;
046    import org.apache.tools.ant.types.Description;
047    import org.apache.tools.ant.types.FileList;
048    import org.apache.tools.ant.types.FileSet;
049    import org.apache.tools.ant.types.Path;
050    import 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     */
075    public 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    }