View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.launcher;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.PrintStream;
23  import java.net.URL;
24  import java.net.URLClassLoader;
25  import java.net.URLDecoder;
26  import java.util.ResourceBundle;
27  
28  import org.apache.commons.launcher.types.ArgumentSet;
29  import org.apache.commons.launcher.types.JVMArgumentSet;
30  import org.apache.commons.launcher.types.SysPropertySet;
31  import org.apache.tools.ant.Main;
32  import org.apache.tools.ant.Project;
33  import org.apache.tools.ant.ProjectHelper;
34  import org.apache.tools.ant.taskdefs.Ant;
35  import org.apache.tools.ant.taskdefs.Available;
36  import org.apache.tools.ant.taskdefs.CallTarget;
37  import org.apache.tools.ant.taskdefs.ConditionTask;
38  import org.apache.tools.ant.taskdefs.ExecTask;
39  import org.apache.tools.ant.taskdefs.Exit;
40  import org.apache.tools.ant.taskdefs.Property;
41  import org.apache.tools.ant.taskdefs.Mkdir;
42  import org.apache.tools.ant.taskdefs.Copy;
43  import org.apache.tools.ant.taskdefs.Delete;
44  import org.apache.tools.ant.taskdefs.Taskdef;
45  import org.apache.tools.ant.taskdefs.WaitFor;
46  import org.apache.tools.ant.types.Description;
47  import org.apache.tools.ant.types.FileList;
48  import org.apache.tools.ant.types.FileSet;
49  import org.apache.tools.ant.types.Path;
50  import org.apache.tools.ant.types.PatternSet;
51  
52  /**
53   * A class that is used to launch a Java process. The primary purpose of this
54   * class is to eliminate the need for a batch or shell script to launch a Java
55   * process. Some situations where elimination of a batch or shell script may be 
56   * desirable are:
57   * <ul>
58   * <li>You want to avoid having to determining where certain application paths
59   *  are e.g. your application's home directory, etc. Determining this
60   *  dynamically in a Windows batch scripts is very tricky on some versions of
61   *  Windows or when softlinks are used on Unix platforms.
62   * <li>You need to enforce certain properties e.g. java.endorsed.dirs when
63   *  running with JDK 1.4.
64   * <li>You want to allow users to pass in custom JVM arguments or system
65   *  properties without having to parse and reorder arguments in your script.
66   *  This can be tricky and/or messy in batch and shell scripts.
67   * <li>You want to bootstrap Java properties from a configuration file instead
68   *  hard-coding them in your batch and shell scripts.
69   * <li>You want to provide localized error messages which is very tricky to do
70   *  in batch and shell scripts.
71   * </ul>
72   *
73   * @author Patrick Luby
74   */
75  public class Launcher implements Runnable {
76  
77      //----------------------------------------------------------- Static Fields
78  
79  
80      /**
81       * Cached bootstrap file.
82       */
83      private static File bootstrapFile = null;
84  
85      /**
86       * Cached java command
87       */
88      private static String javaCmd = null;
89  
90      /**
91       * Cached JDB command
92       */
93      private static String jdbCmd = null;
94  
95      /**
96       * Default XML file name
97       */
98      private final static String DEFAULT_XML_FILE_NAME = "launcher.xml";
99  
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 }