1 /* 2 * Copyright (c) 2004-2010, P. Simon Tuffs (simon@simontuffs.com) 3 * All rights reserved. 4 * 5 * See the full license at http://one-jar.sourceforge.net/one-jar-license.html 6 * This license is also included in the distributions of this software 7 * under doc/one-jar-license.txt 8 */ 9 10 package com.simontuffs.onejar; 11 12 import java.io.BufferedReader; 13 import java.io.File; 14 import java.io.FileInputStream; 15 import java.io.IOException; 16 import java.io.InputStream; 17 import java.io.InputStreamReader; 18 import java.lang.reflect.Constructor; 19 import java.lang.reflect.Method; 20 import java.net.MalformedURLException; 21 import java.net.URL; 22 import java.security.AccessController; 23 import java.security.PrivilegedAction; 24 import java.util.ArrayList; 25 import java.util.Arrays; 26 import java.util.Enumeration; 27 import java.util.Properties; 28 import java.util.jar.Attributes; 29 import java.util.jar.JarEntry; 30 import java.util.jar.JarFile; 31 import java.util.jar.JarInputStream; 32 import java.util.jar.Manifest; 33 import java.util.zip.ZipEntry; 34 import java.util.zip.ZipFile; 35 36 /** 37 * Run a java application which requires multiple support jars from inside 38 * a single jar file. 39 * 40 * <p> 41 * Developer time JVM properties: 42 * <pre> 43 * -Done-jar.main.class={name} Use named class as main class to run. 44 * -Done-jar.record[=recording] Record loaded classes into "recording" directory. 45 * Flatten jar.names into directory tree suitable 46 * for use as a classpath. 47 * -Done-jar.jar.names Record loaded classes, preserve jar structure 48 * -Done-jar.verbose Run the JarClassLoader in verbose mode. 49 * </pre> 50 * @author simon@simontuffs.com (<a href="http://www.simontuffs.com">http://www.simontuffs.com</a>) 51 */ 52 public class Boot { 53 54 /** 55 * The name of the manifest attribute which controls which class 56 * to bootstrap from the jar file. The boot class can 57 * be in any of the contained jar files. 58 */ 59 public final static String BOOT_CLASS = "Boot-Class"; 60 public final static String ONE_JAR_CLASSLOADER = "One-Jar-Class-Loader"; 61 public final static String ONE_JAR_MAIN_CLASS = "One-Jar-Main-Class"; 62 public final static String ONE_JAR_DEFAULT_MAIN_JAR = "One-Jar-Default-Main-Jar"; 63 public final static String ONE_JAR_MAIN_ARGS = "One-Jar-Main-Args"; 64 public final static String ONE_JAR_URL_FACTORY = "One-Jar-URL-Factory"; 65 66 public final static String MANIFEST = "META-INF/MANIFEST.MF"; 67 public final static String MAIN_JAR = "main/main.jar"; 68 69 public final static String WRAP_CLASS_LOADER = "Wrap-Class-Loader"; 70 public final static String WRAP_DIR = "wrap"; 71 public final static String WRAP_JAR = "/" + WRAP_DIR + "/wraploader.jar"; 72 73 // System properties. 74 public final static String PROPERTY_PREFIX = "one-jar."; 75 public final static String P_MAIN_CLASS = PROPERTY_PREFIX + "main.class"; 76 public final static String P_MAIN_JAR = PROPERTY_PREFIX + "main.jar"; 77 public final static String P_MAIN_APP = PROPERTY_PREFIX + "main.app"; 78 public final static String P_RECORD = PROPERTY_PREFIX + "record"; 79 public final static String P_JARNAMES = PROPERTY_PREFIX + "jar.names"; 80 public final static String P_VERBOSE = PROPERTY_PREFIX + "verbose"; 81 public final static String P_INFO = PROPERTY_PREFIX + "info"; 82 public final static String P_WARNING = PROPERTY_PREFIX + "warning"; 83 public final static String P_STATISTICS = PROPERTY_PREFIX + "statistics"; 84 public final static String P_SHOW_PROPERTIES = PROPERTY_PREFIX + "show.properties"; 85 public final static String P_JARPATH = PROPERTY_PREFIX + "jar.path"; 86 public final static String P_ONE_JAR_CLASS_PATH = PROPERTY_PREFIX + "class.path"; 87 public final static String P_JAVA_CLASS_PATH = "java.class.path"; 88 public final static String P_PATH_SEPARATOR = "|"; 89 public final static String P_EXPAND_DIR = PROPERTY_PREFIX + "expand.dir"; 90 91 // Command-line arguments 92 public final static String A_HELP = "--one-jar-help"; 93 public final static String A_VERSION = "--one-jar-version"; 94 95 public final static String[] HELP_PROPERTIES = { 96 P_MAIN_CLASS, "Specifies the name of the class which should be executed \n(via public static void main(String[])", 97 P_MAIN_APP, "Specifies the name of the main/<app>.jar to be executed", 98 P_RECORD, "true: Enables recording of the classes loaded by the application", 99 P_JARNAMES, "true: Recorded classes are kept in directories corresponding to their jar names.\n" + 100 "false: Recorded classes are flattened into a single directory. \nDuplicates are ignored (first wins)", 101 P_VERBOSE, "true: Print verbose classloading information", 102 P_INFO, "true: Print informative classloading information", 103 P_WARNING, "true: Print serious classloading warnings", 104 P_STATISTICS, "true: Shows statistics about the One-Jar Classloader", 105 P_JARPATH, "Full path of the one-jar file being executed. \nOnly needed if java.class.path does not contain the path to the jar, e.g. on Max OS/X.", 106 P_ONE_JAR_CLASS_PATH, "Extra classpaths to be added to the execution environment. \nUse platform independent path separator '" + P_PATH_SEPARATOR + "'", 107 P_EXPAND_DIR, "Directory to use for expanded files.", 108 P_SHOW_PROPERTIES, "true: Shows the JVM system properties.", 109 }; 110 111 public final static String[] HELP_ARGUMENTS = { 112 A_HELP, "Shows this message, then exits.", 113 A_VERSION, "Shows the version of One-JAR, then exits.", 114 }; 115 116 protected static String mainJar; 117 118 protected static boolean warning = true, info, verbose, statistics; 119 protected static String myJarPath; 120 121 protected static long startTime = System.currentTimeMillis(); 122 protected static long endTime = 0; 123 124 125 // Singleton loader. This must not be changed once it is set, otherwise all 126 // sorts of nasty class-cast exceptions will ensue. Hence we control 127 // access to it strongly. 128 private static JarClassLoader loader = null; 129 130 131 /** 132 * This method provides access to the bootstrap One-JAR classloader which 133 * is needed in the URL connection Handler when opening streams relative 134 * to classes. 135 * @return 136 */ getClassLoader()137 public synchronized static JarClassLoader getClassLoader() { 138 return loader; 139 } 140 141 /** 142 * This is the single point of entry for setting the "loader" member. It checks to 143 * make sure programming errors don't call it more than once. 144 * @param $loader 145 */ setClassLoader(JarClassLoader $loader)146 public synchronized static void setClassLoader(JarClassLoader $loader) { 147 if (loader != null) throw new RuntimeException("Attempt to set a second Boot loader"); 148 loader = $loader; 149 } 150 VERBOSE(String message)151 protected static void VERBOSE(String message) { 152 if (verbose) System.out.println("Boot: " + message); 153 } 154 WARNING(String message)155 protected static void WARNING(String message) { 156 System.err.println("Boot: Warning: " + message); 157 } 158 INFO(String message)159 protected static void INFO(String message) { 160 if (info) System.out.println("Boot: Info: " + message); 161 } 162 PRINTLN(String message)163 protected static void PRINTLN(String message) { 164 System.out.println("Boot: " + message); 165 } 166 main(String[] args)167 public static void main(String[] args) throws Exception { 168 run(args); 169 } 170 run(String args[])171 public static void run(String args[]) throws Exception { 172 173 args = processArgs(args); 174 175 // Is the main class specified on the command line? If so, boot it. 176 // Otherwise, read the main class out of the manifest. 177 String mainClass = null; 178 179 { 180 // Default properties are in resource 'one-jar.properties'. 181 Properties properties = new Properties(); 182 String props = "one-jar.properties"; 183 InputStream is = Boot.class.getResourceAsStream("/" + props); 184 if (is != null) { 185 INFO("loading properties from " + props); 186 properties.load(is); 187 } 188 189 // Merge in anything in a local file with the same name. 190 if (new File(props).exists()) { 191 is = new FileInputStream(props); 192 if (is != null) { 193 INFO("merging properties from " + props); 194 properties.load(is); 195 } 196 } 197 198 // Set system properties only if not already specified. 199 Enumeration _enum = properties.propertyNames(); 200 while (_enum.hasMoreElements()) { 201 String name = (String)_enum.nextElement(); 202 if (System.getProperty(name) == null) { 203 System.setProperty(name, properties.getProperty(name)); 204 } 205 } 206 } 207 if (Boolean.valueOf(System.getProperty(P_SHOW_PROPERTIES, "false")).booleanValue()) { 208 // What are the system properties. 209 Properties props = System.getProperties(); 210 String keys[] = (String[])props.keySet().toArray(new String[]{}); 211 Arrays.sort(keys); 212 213 for (int i=0; i<keys.length; i++) { 214 String key = keys[i]; 215 System.out.println(key + "=" + props.get(key)); 216 } 217 } 218 219 // Process developer properties: 220 if (mainClass == null) { 221 mainClass = System.getProperty(P_MAIN_CLASS); 222 } 223 224 if (mainJar == null) { 225 String app = System.getProperty(P_MAIN_APP); 226 if (app != null) { 227 mainJar = "main/" + app + ".jar"; 228 } else { 229 mainJar = System.getProperty(P_MAIN_JAR, MAIN_JAR); 230 } 231 } 232 233 // Pick some things out of the top-level JAR file. 234 String jar = getMyJarPath(); 235 JarFile jarFile = new JarFile(jar); 236 Manifest manifest = jarFile.getManifest(); 237 Attributes attributes = manifest.getMainAttributes(); 238 String bootLoaderName = attributes.getValue(ONE_JAR_CLASSLOADER); 239 240 if (mainJar == null) { 241 mainJar = attributes.getValue(ONE_JAR_DEFAULT_MAIN_JAR); 242 } 243 244 String mainargs = attributes.getValue(ONE_JAR_MAIN_ARGS); 245 if (mainargs != null && args.length == 0) { 246 // Replace the args with built-in. Support escaped whitespace. 247 args = mainargs.split("[^\\\\]\\s"); 248 for (int i=0; i<args.length; i++) { 249 args[i] = args[i].replaceAll("\\\\(\\s)", "$1"); 250 } 251 } 252 253 // If no main-class specified, check the manifest of the main jar for 254 // a Boot-Class attribute. 255 if (mainClass == null) { 256 mainClass = attributes.getValue(ONE_JAR_MAIN_CLASS); 257 if (mainClass == null) { 258 mainClass = attributes.getValue(BOOT_CLASS); 259 if (mainClass != null) { 260 WARNING("The manifest attribute " + BOOT_CLASS + " is deprecated in favor of the attribute " + ONE_JAR_MAIN_CLASS); 261 } 262 } 263 } 264 265 if (mainClass == null) { 266 // Still don't have one (default). One final try: look for a jar file in a 267 // main directory. There should be only one, and it's manifest 268 // Main-Class attribute is the main class. The JarClassLoader will take 269 // care of finding it. 270 InputStream is = Boot.class.getResourceAsStream("/" + mainJar); 271 if (is != null) { 272 JarInputStream jis = new JarInputStream(is); 273 Manifest mainmanifest = jis.getManifest(); 274 jis.close(); 275 mainClass = mainmanifest.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS); 276 } else { 277 // There is no main jar. Info unless mainJar is empty string. 278 // The load(mainClass) will scan for main jars anyway. 279 if (!"".equals(mainJar)){ 280 INFO("Unable to locate main jar '" + mainJar + "' in the JAR file " + getMyJarPath()); 281 } 282 } 283 } 284 285 // Do we need to create a wrapping classloader? Check for the 286 // presence of a "wrap" directory at the top of the jar file. 287 URL url = Boot.class.getResource(WRAP_JAR); 288 289 if (url != null) { 290 // Wrap class loaders. 291 final JarClassLoader bootLoader = getBootLoader(bootLoaderName); 292 bootLoader.load(null); 293 294 // Read the "Wrap-Class-Loader" property from the wraploader jar file. 295 // This is the class to use as a wrapping class-loader. 296 InputStream is = Boot.class.getResourceAsStream(WRAP_JAR); 297 if (is != null) { 298 JarInputStream jis = new JarInputStream(is); 299 final String wrapLoader = jis.getManifest().getMainAttributes().getValue(WRAP_CLASS_LOADER); 300 jis.close(); 301 if (wrapLoader == null) { 302 WARNING(url + " did not contain a " + WRAP_CLASS_LOADER + " attribute, unable to load wrapping classloader"); 303 } else { 304 INFO("using " + wrapLoader); 305 JarClassLoader wrapped = getWrapLoader(bootLoader, wrapLoader); 306 if (wrapped == null) { 307 WARNING("Unable to instantiate " + wrapLoader + " from " + WRAP_DIR + ": using default JarClassLoader"); 308 wrapped = getBootLoader(null); 309 } 310 setClassLoader(wrapped); 311 } 312 } 313 } else { 314 setClassLoader(getBootLoader(bootLoaderName, Boot.class.getClassLoader())); 315 INFO("using JarClassLoader: " + getClassLoader().getClass().getName()); 316 } 317 318 // Allow injection of the URL factory. 319 String urlfactory = attributes.getValue(ONE_JAR_URL_FACTORY); 320 if (urlfactory != null) { 321 loader.setURLFactory(urlfactory); 322 } 323 324 mainClass = loader.load(mainClass); 325 326 if (mainClass == null && !loader.isExpanded()) 327 throw new Exception(getMyJarName() + " main class was not found (fix: add main/main.jar with a Main-Class manifest attribute, or specify -D" + P_MAIN_CLASS + "=<your.class.name>), or use " + ONE_JAR_MAIN_CLASS + " in the manifest"); 328 329 if (mainClass != null) { 330 // Guard against the main.jar pointing back to this 331 // class, and causing an infinite recursion. 332 String bootClass = Boot.class.getName(); 333 if (bootClass.equals(mainClass)) 334 throw new Exception(getMyJarName() + " main class (" + mainClass + ") would cause infinite recursion: check main.jar/META-INF/MANIFEST.MF/Main-Class attribute: " + mainClass); 335 336 Class cls = loader.loadClass(mainClass); 337 338 endTime = System.currentTimeMillis(); 339 showTime(); 340 341 Method main = cls.getMethod("main", new Class[]{String[].class}); 342 main.invoke(null, new Object[]{args}); 343 } 344 } 345 showTime()346 public static void showTime() { 347 long endtime = System.currentTimeMillis(); 348 if (statistics) { 349 PRINTLN("Elapsed time: " + (endtime - startTime) + "ms"); 350 } 351 } 352 setProperties(IProperties jarloader)353 public static void setProperties(IProperties jarloader) { 354 INFO("setProperties(" + jarloader + ")"); 355 if (getProperty(P_RECORD, "false")) { 356 jarloader.setRecord(true); 357 jarloader.setRecording(System.getProperty(P_RECORD)); 358 } 359 if (getProperty(P_JARNAMES, "false")) { 360 jarloader.setRecord(true); 361 jarloader.setFlatten(false); 362 } 363 // TODO: clean up the use of one-jar.{verbose,info,warning} properties. 364 if (verbose = getProperty(P_VERBOSE, "false")) { 365 jarloader.setVerbose(true); 366 jarloader.setInfo(true); 367 } 368 jarloader.setInfo(info=getProperty(P_INFO, "false")); 369 jarloader.setWarning(warning=getProperty(P_WARNING, "true")); 370 371 statistics = getProperty(P_STATISTICS, "false"); 372 } 373 getProperty(String key, String $default)374 public static boolean getProperty(String key, String $default) { 375 return Boolean.valueOf(System.getProperty(key, "false")).booleanValue(); 376 } 377 getMyJarName()378 public static String getMyJarName() { 379 String name = getMyJarPath(); 380 int last = name.lastIndexOf("/"); 381 if (last >= 0) { 382 name = name.substring(last+1); 383 } 384 return name; 385 } 386 getMyJarPath()387 public static String getMyJarPath() { 388 if (myJarPath != null) { 389 return myJarPath; 390 } 391 myJarPath = System.getProperty(P_JARPATH); 392 if (myJarPath == null) { 393 try { 394 // Hack to obtain the name of this jar file. 395 String jarname = System.getProperty(P_JAVA_CLASS_PATH); 396 // Open each Jar file looking for this class name. This allows for 397 // JVM's that place more than the jar file on the classpath. 398 String jars[] =jarname.split(System.getProperty("path.separator")); 399 for (int i=0; i<jars.length; i++) { 400 jarname = jars[i]; 401 VERBOSE("Checking " + jarname + " as One-Jar file"); 402 // Allow for URL based paths, as well as file-based paths. File 403 InputStream is = null; 404 try { 405 is = new URL(jarname).openStream(); 406 } catch (MalformedURLException mux) { 407 // Try a local file. 408 try { 409 is = new FileInputStream(jarname); 410 } catch (IOException iox) { 411 // Ignore..., but it isn't good to have bad entries on the classpath. 412 continue; 413 } 414 } 415 ZipEntry entry = findJarEntry(new JarInputStream(is), Boot.class.getName().replace('.', '/') + ".class"); 416 if (entry != null) { 417 myJarPath = jarname; 418 break; 419 } else { 420 // One more try as a Zip file: supports launch4j on Windows. 421 entry = findZipEntry(new ZipFile(jarname), Boot.class.getName().replace('.', '/') + ".class"); 422 if (entry != null) { 423 myJarPath = jarname; 424 break; 425 } 426 } 427 } 428 } catch (Exception x) { 429 x.printStackTrace(); 430 WARNING("jar=" + myJarPath + " loaded from " + P_JAVA_CLASS_PATH + " (" + System.getProperty(P_JAVA_CLASS_PATH) + ")"); 431 } 432 } 433 if (myJarPath == null) { 434 throw new IllegalArgumentException("Unable to locate " + Boot.class.getName() + " in the java.class.path: consider using -D" + P_JARPATH + " to specify the one-jar filename."); 435 } 436 // Normalize those annoying DOS backslashes. 437 myJarPath = myJarPath.replace('\\', '/'); 438 return myJarPath; 439 } 440 findJarEntry(JarInputStream jis, String name)441 public static JarEntry findJarEntry(JarInputStream jis, String name) throws IOException { 442 JarEntry entry; 443 while ((entry = jis.getNextJarEntry()) != null) { 444 if (entry.getName().equals(name)) { 445 return entry; 446 } 447 } 448 return null; 449 } 450 findZipEntry(ZipFile zip, String name)451 public static ZipEntry findZipEntry(ZipFile zip, String name) throws IOException { 452 Enumeration entries = zip.entries(); 453 while (entries.hasMoreElements()) { 454 ZipEntry entry = (ZipEntry) entries.nextElement(); 455 VERBOSE(("findZipEntry(): entry=" + entry.getName())); 456 if (entry.getName().equals(name)) 457 return entry; 458 } 459 return null; 460 } 461 firstWidth(String[] table)462 public static int firstWidth(String[] table) { 463 int width = 0; 464 for (int i=0; i<table.length; i+=2) { 465 if (table[i].length() > width) width = table[i].length(); 466 } 467 return width; 468 } 469 pad(String indent, String string, int width)470 public static String pad(String indent, String string, int width) { 471 StringBuffer buf = new StringBuffer(); 472 buf.append(indent); 473 buf.append(string); 474 for (int i=0; i<width-string.length(); i++) { 475 buf.append(" "); 476 } 477 return buf.toString(); 478 } 479 wrap(String indent, String string, int width)480 public static String wrap(String indent, String string, int width) { 481 String padding = pad(indent, "", width); 482 string = string.replaceAll("\n", "\n" + padding); 483 return string; 484 } 485 processArgs(String args[])486 public static String[] processArgs(String args[]) throws Exception { 487 // Check for arguments which matter to us, and strip them. 488 VERBOSE("processArgs(" + Arrays.asList(args) + ")"); 489 ArrayList list = new ArrayList(); 490 for (int a=0; a<args.length; a++) { 491 String argument = args[a]; 492 if (argument.startsWith(A_HELP)) { 493 int width = firstWidth(HELP_ARGUMENTS); 494 // Width of first column 495 496 System.out.println("One-Jar uses the following command-line arguments"); 497 for (int i=0; i<HELP_ARGUMENTS.length; i++) { 498 System.out.print(pad(" ", HELP_ARGUMENTS[i++], width+1)); 499 System.out.println(wrap(" ", HELP_ARGUMENTS[i], width+1)); 500 } 501 System.out.println(); 502 503 width = firstWidth(HELP_PROPERTIES); 504 System.out.println("One-Jar uses the following VM properties (-D<property>=<true|false|string>)"); 505 for (int i=0; i<HELP_PROPERTIES.length; i++) { 506 System.out.print(pad(" ", HELP_PROPERTIES[i++], width+1)); 507 System.out.println(wrap(" ", HELP_PROPERTIES[i], width+1)); 508 } 509 System.out.println(); 510 System.exit(0); 511 } else if (argument.startsWith(A_VERSION)) { 512 InputStream is = Boot.class.getResourceAsStream("/.version"); 513 if (is != null) { 514 BufferedReader br = new BufferedReader(new InputStreamReader(is)); 515 String version = br.readLine(); 516 br.close(); 517 System.out.println("One-JAR version " + version); 518 } else { 519 System.out.println("Unable to determine One-JAR version (missing /.version resource in One-JAR archive)"); 520 } 521 System.exit(0); 522 } else { 523 list.add(argument); 524 } 525 } 526 return (String[])list.toArray(new String[0]); 527 } 528 getBootLoader(final String loader)529 protected static JarClassLoader getBootLoader(final String loader) { 530 JarClassLoader bootLoader = (JarClassLoader)AccessController.doPrivileged( 531 new PrivilegedAction() { 532 public Object run() { 533 if (loader != null) { 534 try { 535 Class cls = Class.forName(loader); 536 Constructor ctor = cls.getConstructor(new Class[]{String.class}); 537 return ctor.newInstance(new Object[]{WRAP_DIR}); 538 } catch (Exception x) { 539 WARNING("Unable to instantiate " + loader + ": " + x + " continuing using default " + JarClassLoader.class.getName()); 540 } 541 } 542 return new JarClassLoader(WRAP_DIR); 543 } 544 } 545 ); 546 return bootLoader; 547 } 548 getBootLoader(final String loader, ClassLoader parent)549 protected static JarClassLoader getBootLoader(final String loader, ClassLoader parent) { 550 return (JarClassLoader)AccessController.doPrivileged( 551 new PrivilegedAction() { 552 public Object run() { 553 if (loader != null) { 554 try { 555 Class cls = Class.forName(loader); 556 Constructor ctor = cls.getConstructor(new Class[]{ClassLoader.class}); 557 return ctor.newInstance(new Object[]{Boot.class.getClassLoader()}); 558 } catch (Exception x) { 559 WARNING("Unable to instantiate " + loader + ": " + x + " continuing using default " + JarClassLoader.class.getName()); 560 } 561 } 562 return new JarClassLoader(Boot.class.getClassLoader()); 563 } 564 } 565 ); 566 } 567 568 protected static JarClassLoader getWrapLoader(final ClassLoader bootLoader, final String wrapLoader) { 569 return ((JarClassLoader)AccessController.doPrivileged( 570 new PrivilegedAction() { 571 public Object run() { 572 try { 573 Class jarLoaderClass = bootLoader.loadClass(wrapLoader); 574 Constructor ctor = jarLoaderClass.getConstructor(new Class[]{ClassLoader.class}); 575 return ctor.newInstance(new Object[]{bootLoader}); 576 } catch (Throwable t) { 577 WARNING(t.toString()); 578 } 579 return null; 580 } 581 })); 582 } 583 584 public static long getEndTime() { 585 return endTime; 586 } 587 588 public static long getStartTime() { 589 return startTime; 590 } 591 592 } 593