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}