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 /** 11 * Many thanks to the following for their contributions to One-Jar: 12 * 13 * Contributor: Christopher Ottley <xknight@users.sourceforge.net> 14 * Contributor: Thijs Sujiten (www.semantica.nl) 15 * Contributor: Gerold Friedmann 16 */ 17 18 package com.simontuffs.onejar; 19 20 import java.io.BufferedReader; 21 import java.io.ByteArrayInputStream; 22 import java.io.ByteArrayOutputStream; 23 import java.io.File; 24 import java.io.FileOutputStream; 25 import java.io.IOException; 26 import java.io.InputStream; 27 import java.io.InputStreamReader; 28 import java.io.OutputStream; 29 import java.net.MalformedURLException; 30 import java.net.URL; 31 import java.net.URLClassLoader; 32 import java.net.URLConnection; 33 import java.net.URLStreamHandler; 34 import java.security.CodeSource; 35 import java.security.ProtectionDomain; 36 import java.security.cert.Certificate; 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.Collections; 40 import java.util.Date; 41 import java.util.Enumeration; 42 import java.util.HashMap; 43 import java.util.HashSet; 44 import java.util.Iterator; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Set; 48 import java.util.jar.Attributes; 49 import java.util.jar.JarEntry; 50 import java.util.jar.JarFile; 51 import java.util.jar.JarInputStream; 52 import java.util.jar.Manifest; 53 import java.util.jar.Attributes.Name; 54 import java.util.regex.Matcher; 55 import java.util.regex.Pattern; 56 57 /** 58 * Loads classes from pre-defined locations inside the jar file containing this 59 * class. Classes will be loaded from jar files contained in the following 60 * locations within the main jar file (on the classpath of the application 61 * actually, which when running with the "java -jar" command works out to be 62 * the same thing). 63 * <ul> 64 * <li> 65 * /lib Used to contain library jars. 66 * </li> 67 * <li> 68 * /main Used to contain a default main jar. 69 * </li> 70 * </ul> 71 * @author simon@simontuffs.com (<a href="http://www.simontuffs.com">http://www.simontuffs.com</a>) 72 */ 73 public class JarClassLoader extends ClassLoader implements IProperties { 74 75 public final static String LIB_PREFIX = "lib/"; 76 public final static String BINLIB_PREFIX = "binlib/"; 77 public final static String MAIN_PREFIX = "main/"; 78 public final static String RECORDING = "recording"; 79 public final static String TMP = "tmp"; 80 public final static String UNPACK = "unpack"; 81 public final static String EXPAND = "One-Jar-Expand"; 82 public final static String EXPAND_DIR = "One-Jar-Expand-Dir"; 83 public final static String SHOW_EXPAND = "One-Jar-Show-Expand"; 84 public final static String CONFIRM_EXPAND = "One-Jar-Confirm-Expand"; 85 public final static String CLASS = ".class"; 86 87 public final static String NL = System.getProperty("line.separator"); 88 89 public final static String JAVA_PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; 90 91 protected String name; 92 protected boolean noExpand, expanded; 93 protected ClassLoader externalClassLoader; 94 95 static { 96 // Add our 'onejar:' protocol handler, but leave open the 97 // possibility of a subsequent class taking over the 98 // factory. TODO: (how reasonable is this?) 99 String handlerPackage = System.getProperty(JAVA_PROTOCOL_HANDLER); 100 if (handlerPackage == null) handlerPackage = ""; 101 if (handlerPackage.length() > 0) handlerPackage = "|" + handlerPackage; 102 handlerPackage = "com.simontuffs" + handlerPackage; System.setProperty(JAVA_PROTOCOL_HANDLER, handlerPackage)103 System.setProperty(JAVA_PROTOCOL_HANDLER, handlerPackage); 104 105 } 106 PREFIX()107 protected String PREFIX() { 108 return "JarClassLoader: "; 109 } 110 NAME()111 protected String NAME() { 112 return (name != null? "'" + name + "' ": ""); 113 } 114 VERBOSE(String message)115 protected void VERBOSE(String message) { 116 if (verbose) System.out.println(PREFIX() + NAME() + message); 117 } 118 WARNING(String message)119 protected void WARNING(String message) { 120 if (warning) System.err.println(PREFIX() + "Warning: " + NAME() + message); 121 } 122 INFO(String message)123 protected void INFO(String message) { 124 if (info) System.out.println(PREFIX() + "Info: " + NAME() + message); 125 } 126 PRINTLN(String message)127 protected void PRINTLN(String message) { 128 System.out.println(message); 129 } 130 PRINT(String message)131 protected void PRINT(String message) { 132 System.out.print(message); 133 } 134 135 // Synchronize for thread safety. This is less important until we 136 // start to do lazy loading, but it's a good idea anyway. 137 protected Map byteCode = Collections.synchronizedMap(new HashMap()); 138 protected Map pdCache = Collections.synchronizedMap(new HashMap()); 139 protected Map binLibPath = Collections.synchronizedMap(new HashMap()); 140 protected Set jarNames = Collections.synchronizedSet(new HashSet()); 141 142 protected boolean record = false, flatten = false, unpackFindResource = false; 143 protected boolean verbose = false, info = false, warning = true; 144 protected String recording = RECORDING; 145 146 protected String jarName, mainJar, wrapDir; 147 protected boolean delegateToParent; 148 149 protected static class ByteCode { ByteCode(String $name, String $original, ByteArrayOutputStream baos, String $codebase, Manifest $manifest)150 public ByteCode(String $name, String $original, ByteArrayOutputStream baos, String $codebase, Manifest $manifest) { 151 name = $name; 152 original = $original; 153 bytes = baos.toByteArray(); 154 codebase = $codebase; 155 manifest = $manifest; 156 } 157 public byte bytes[]; 158 public String name, original, codebase; 159 public Manifest manifest; 160 } 161 162 163 /** 164 * Create a non-delegating but jar-capable classloader for bootstrap 165 * purposes. 166 * @param $wrap The directory in the archive from which to load a 167 * wrapping classloader. 168 */ JarClassLoader(String $wrap)169 public JarClassLoader(String $wrap) { 170 wrapDir = $wrap; 171 delegateToParent = wrapDir == null; 172 Boot.setProperties(this); 173 init(); 174 } 175 176 /** 177 * The main constructor for the Jar-capable classloader. 178 * @param $record If true, the JarClassLoader will record all used classes 179 * into a recording directory (called 'recording' by default) 180 * The name of each jar file will be used as a directory name 181 * for the recorded classes. 182 * @param $flatten Whether to flatten out the recorded classes (i.e. eliminate 183 * the jar-file name from the recordings). 184 * 185 * Example: Given the following layout of the one-jar.jar file 186 * <pre> 187 * / 188 * /META-INF 189 * | MANIFEST.MF 190 * /com 191 * /simontuffs 192 * /onejar 193 * Boot.class 194 * JarClassLoader.class 195 * /main 196 * main.jar 197 * /com 198 * /main 199 * Main.class 200 * /lib 201 * util.jar 202 * /com 203 * /util 204 * Util.clas 205 * </pre> 206 * The recording directory will look like this: 207 * <ul> 208 * <li>flatten=false</li> 209 * <pre> 210 * /recording 211 * /main.jar 212 * /com 213 * /main 214 * Main.class 215 * /util.jar 216 * /com 217 * /util 218 * Util.class 219 * </pre> 220 * 221 * <li>flatten = true</li> 222 * <pre> 223 * /recording 224 * /com 225 * /main 226 * Main.class 227 * /util 228 * Util.class 229 * 230 * </ul> 231 * Flatten mode is intended for when you want to create a super-jar which can 232 * be launched directly without using one-jar's launcher. Run your application 233 * under all possible scenarios to collect the actual classes which are loaded, 234 * then jar them all up, and point to the main class with a "Main-Class" entry 235 * in the manifest. 236 * 237 */ JarClassLoader(ClassLoader parent)238 public JarClassLoader(ClassLoader parent) { 239 super(parent); 240 delegateToParent = true; 241 Boot.setProperties(this); 242 init(); 243 // System.out.println(PREFIX() + this + " parent=" + parent + " loaded by " + this.getClass().getClassLoader()); 244 } 245 246 protected static ThreadLocal current = new ThreadLocal(); 247 /** 248 * Common initialization code: establishes a classloader for delegation 249 * to one-jar.class.path resources. 250 */ init()251 protected void init() { 252 String classpath = System.getProperty(Boot.P_ONE_JAR_CLASS_PATH); 253 if (classpath != null) { 254 String tokens[] = classpath.split("\\" + Boot.P_PATH_SEPARATOR); 255 List list = new ArrayList(); 256 for (int i=0; i<tokens.length; i++) { 257 String path = tokens[i]; 258 try { 259 list.add(new URL(path)); 260 } catch (MalformedURLException mux) { 261 // Try a file:// prefix and an absolute path. 262 try { 263 String _path = new File(path).getCanonicalPath(); 264 // URLClassLoader searches in a directory if and only if the path ends with '/': 265 // toURI() takes care of adding the trailing slash in this case so everything's ok 266 list.add(new File(_path).toURI().toURL()); 267 } catch (MalformedURLException ignore) { 268 Boot.WARNING("Unable to parse external path: " + path); 269 } catch (IOException ignore) { 270 Boot.WARNING("Unable to parse external path: " + path); 271 } catch (IllegalArgumentException ignore) { 272 // won't happen File.toURI() returns an absolute URI 273 Boot.WARNING("Unable to parse external path: " + path); 274 } 275 } 276 } 277 URL urls[] = (URL[])list.toArray(new URL[0]); 278 Boot.INFO("external URLs=" + Arrays.asList(urls)); 279 // BUG-2833948 280 // Delegate back into this classloader, use ThreadLocal to avoid recursion. 281 externalClassLoader = new URLClassLoader(urls, this) { 282 // Handle recursion for classes, and mutual recursion for resources. 283 final static String LOAD_CLASS = "loadClass():"; 284 final static String GET_RESOURCE = "getResource():"; 285 final static String FIND_RESOURCE = "findResource():"; 286 // Protect entry points which could lead to recursion. Strangely 287 // inelegant because you can't proxy a class. Or use closures. 288 public Class loadClass(String name) throws ClassNotFoundException { 289 if (reentered(LOAD_CLASS + name)) { 290 throw new ClassNotFoundException(name); 291 } 292 VERBOSE("externalClassLoader.loadClass(" + name + ")"); 293 Object old = current.get(); 294 current.set(LOAD_CLASS + name); 295 try { 296 return super.loadClass(name); 297 } finally { 298 current.set(old); 299 } 300 } 301 public URL getResource(String name) { 302 if (reentered(GET_RESOURCE + name)) 303 return null; 304 VERBOSE("externalClassLoader.getResource(" + name + ")"); 305 Object old = current.get(); 306 current.set(GET_RESOURCE + name); 307 try { 308 return super.getResource(name); 309 } finally { 310 current.set(old); 311 } 312 } 313 public URL findResource(String name) { 314 if (reentered(FIND_RESOURCE + name)) 315 return null; 316 VERBOSE("externalClassLoader.findResource(" + name + ")"); 317 Object old = current.get(); 318 current.set(name); 319 try { 320 current.set(FIND_RESOURCE + name); 321 return super.findResource(name); 322 } finally { 323 current.set(old); 324 } 325 } 326 protected boolean reentered(String name) { 327 // Defend against null name: not sure about semantics there. 328 Object old = current.get(); 329 return old != null && old.equals(name); 330 } 331 }; 332 } 333 } 334 load(String mainClass)335 public String load(String mainClass) { 336 // Hack: if there is a one-jar.jarname property, use it. 337 String jarname = Boot.getMyJarPath(); 338 return load(mainClass, jarname); 339 } 340 load(String mainClass, String jarName)341 public String load(String mainClass, String jarName) { 342 VERBOSE("load("+mainClass+","+jarName+")"); 343 if (record) { 344 new File(recording).mkdirs(); 345 } 346 try { 347 if (jarName == null) { 348 jarName = Boot.getMyJarPath(); 349 } 350 JarFile jarFile = new JarFile(jarName); 351 Enumeration _enum = jarFile.entries(); 352 Manifest manifest = jarFile.getManifest(); 353 String expandPaths[] = null; 354 // TODO: Allow a destination directory (relative or absolute) to 355 // be specified like this: 356 // One-Jar-Expand: build=../expanded 357 String expand = manifest.getMainAttributes().getValue(EXPAND); 358 String expanddir = System.getProperty(Boot.P_EXPAND_DIR); 359 if (expanddir == null) { 360 expanddir = manifest.getMainAttributes().getValue(EXPAND_DIR); 361 } 362 // Default is to expand into temporary directory based on the name of the jar file. 363 if (expanddir == null) { 364 String jar = new File(jarName).getName().replaceFirst("\\.[^\\.]*$", ""); 365 expanddir = "${java.io.tmpdir}/" + jar; 366 } 367 // Expand system properties. 368 expanddir = replaceProps(System.getProperties(), expanddir); 369 370 // Make a note of this location in the VM system properties in case applications need to know 371 // where the expanded files are. 372 System.setProperty(Boot.P_EXPAND_DIR, expanddir); 373 374 boolean shouldExpand = true; 375 File tmpdir = new File(expanddir); 376 if (noExpand == false && expand != null) { 377 expanded = true; 378 VERBOSE(EXPAND + "=" + expand); 379 expandPaths = expand.split(","); 380 boolean getconfirm = Boolean.TRUE.toString().equals(manifest.getMainAttributes().getValue(CONFIRM_EXPAND)); 381 if (getconfirm) { 382 String answer = getConfirmation(tmpdir); 383 if (answer == null) answer = "n"; 384 answer = answer.trim().toLowerCase(); 385 if (answer.startsWith("q")) { 386 PRINTLN("exiting without expansion."); 387 // Indicate (expected) failure with a non-zero return code. 388 System.exit(1); 389 } else if (answer.startsWith("n")) { 390 shouldExpand = false; 391 } 392 } 393 } 394 boolean showexpand = Boolean.TRUE.toString().equals(manifest.getMainAttributes().getValue(SHOW_EXPAND)); 395 if (showexpand) { 396 PRINTLN("Expanding to: " + tmpdir.getAbsolutePath()); 397 } 398 while (_enum.hasMoreElements()) { 399 JarEntry entry = (JarEntry)_enum.nextElement(); 400 if (entry.isDirectory()) continue; 401 402 // The META-INF/MANIFEST.MF file can contain a property which names 403 // directories in the JAR to be expanded (comma separated). For example: 404 // One-Jar-Expand: build,tmp,webapps 405 String $entry = entry.getName(); 406 if (expandPaths != null) { 407 // TODO: Can't think of a better way to do this right now. 408 // This code really doesn't need to be optimized anyway. 409 if (shouldExpand && shouldExpand(expandPaths, $entry)) { 410 File dest = new File(tmpdir, $entry); 411 // Override if ZIP file is newer than existing. 412 if (!dest.exists() || dest.lastModified() < entry.getTime()) { 413 String msg = "Expanding: " + $entry; 414 if (showexpand) { 415 PRINTLN(msg); 416 } else { 417 INFO(msg); 418 } 419 if (dest.exists()) INFO("Update because lastModified=" + new Date(dest.lastModified()) + ", entry=" + new Date(entry.getTime())); 420 File parent = dest.getParentFile(); 421 if (parent != null) { 422 parent.mkdirs(); 423 } 424 VERBOSE("using jarFile.getInputStream(" + entry + ")"); 425 InputStream is = jarFile.getInputStream(entry); 426 FileOutputStream os = new FileOutputStream(dest); 427 copy(is, os); 428 is.close(); 429 os.close(); 430 } else { 431 String msg = "Up-to-date: " + $entry; 432 if (showexpand) { 433 PRINTLN(msg); 434 } else { 435 VERBOSE(msg); 436 } 437 } 438 } 439 } 440 441 if (wrapDir != null && $entry.startsWith(wrapDir) || $entry.startsWith(LIB_PREFIX) || $entry.startsWith(MAIN_PREFIX)) { 442 if (wrapDir != null && !entry.getName().startsWith(wrapDir)) continue; 443 // Load it! 444 VERBOSE("caching " + $entry); 445 VERBOSE("using jarFile.getInputStream(" + entry + ")"); 446 { 447 // Note: loadByteCode consumes the input stream, so make sure its scope 448 // does not extend beyond here. 449 InputStream is = jarFile.getInputStream(entry); 450 if (is == null) 451 throw new IOException("Unable to load resource /" + $entry + " using " + this); 452 loadByteCode(is, $entry, null); 453 } 454 455 // Do we need to look for a main class? 456 if ($entry.startsWith(MAIN_PREFIX)) { 457 if (mainClass == null) { 458 JarInputStream jis = new JarInputStream(jarFile.getInputStream(entry)); 459 Manifest m = jis.getManifest(); 460 jis.close(); 461 // Is this a jar file with a manifest? 462 if (m != null) { 463 mainClass = jis.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS); 464 mainJar = $entry; 465 } 466 } else if (mainJar != null) { 467 WARNING("A main class is defined in multiple jar files inside " + MAIN_PREFIX + mainJar + " and " + $entry); 468 WARNING("The main class " + mainClass + " from " + mainJar + " will be used"); 469 } 470 } 471 } else if (wrapDir == null && $entry.startsWith(UNPACK)) { 472 // Unpack into a temporary directory which is on the classpath of 473 // the application classloader. Badly designed code which relies on the 474 // application classloader can be made to work in this way. 475 InputStream is = this.getClass().getResourceAsStream("/" + $entry); 476 if (is == null) throw new IOException($entry); 477 // Make a sentinel. 478 File dir = new File(TMP); 479 File sentinel = new File(dir, $entry.replace('/', '.')); 480 if (!sentinel.exists()) { 481 INFO("unpacking " + $entry + " into " + dir.getCanonicalPath()); 482 loadByteCode(is, $entry, TMP); 483 sentinel.getParentFile().mkdirs(); 484 sentinel.createNewFile(); 485 } 486 } else if ($entry.endsWith(CLASS)) { 487 // A plain vanilla class file rooted at the top of the jar file. 488 loadBytes(entry, jarFile.getInputStream(entry), "/", null, manifest); 489 VERBOSE("One-Jar class: " + jarFile.getName() + "!/" + entry.getName()); 490 } else { 491 // A resource? 492 loadBytes(entry, jarFile.getInputStream(entry), "/", null, manifest); 493 VERBOSE("One-Jar resource: " + jarFile.getName() + "!/" + entry.getName()); 494 } 495 } 496 // If mainClass is still not defined, return null. The caller is then responsible 497 // for determining a main class. 498 499 } catch (IOException iox) { 500 System.err.println("Unable to load resource: " + iox); 501 iox.printStackTrace(System.err); 502 } 503 return mainClass; 504 } 505 replaceProps(Map replace, String string)506 public String replaceProps(Map replace, String string) { 507 Pattern pat = Pattern.compile("\\$\\{([^\\}]*)"); 508 Matcher mat = pat.matcher(string); 509 boolean found = mat.find(); 510 Map props = new HashMap(); 511 while (found) { 512 String prop = mat.group(1); 513 props.put(prop, replace.get(prop)); 514 found = mat.find(); 515 } 516 Set keys = props.keySet(); 517 Iterator iter = props.keySet().iterator(); 518 while (iter.hasNext()) { 519 String prop = (String)iter.next(); 520 string = string.replace("${" + prop + "}", (String)props.get(prop)); 521 } 522 return string; 523 } 524 shouldExpand(String expandPaths[], String name)525 public static boolean shouldExpand(String expandPaths[], String name) { 526 for (int i=0; i<expandPaths.length; i++) { 527 if (name.startsWith(expandPaths[i])) return true; 528 } 529 return false; 530 } 531 loadByteCode(InputStream is, String jar, String tmp)532 protected void loadByteCode(InputStream is, String jar, String tmp) throws IOException { 533 JarInputStream jis = new JarInputStream(is); 534 JarEntry entry = null; 535 // TODO: implement lazy loading of bytecode. 536 Manifest manifest = jis.getManifest(); 537 if (manifest == null) { 538 WARNING("Null manifest from input stream associated with: " + jar); 539 } 540 while ((entry = jis.getNextJarEntry()) != null) { 541 // if (entry.isDirectory()) continue; 542 loadBytes(entry, jis, jar, tmp, manifest); 543 } 544 // Add in a fake manifest entry. 545 if (manifest != null) { 546 entry = new JarEntry(Boot.MANIFEST); 547 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 548 manifest.write(baos); 549 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); 550 loadBytes(entry, bais, jar, tmp, manifest); 551 } 552 553 } 554 loadBytes(JarEntry entry, InputStream is, String jar, String tmp, Manifest man)555 protected void loadBytes(JarEntry entry, InputStream is, String jar, String tmp, Manifest man) throws IOException { 556 String entryName = entry.getName(); 557 int index = entryName.lastIndexOf('.'); 558 String type = entryName.substring(index+1); 559 560 // agattung: patch (for one-jar 0.95) 561 // add package handling to avoid NullPointer exceptions 562 // after calls to getPackage method of this ClassLoader 563 int index2 = entryName.lastIndexOf('/', index-1); 564 if (entryName.endsWith(CLASS) && index2 > -1) { 565 String packageName = entryName.substring(0, index2).replace('/', '.'); 566 if (getPackage(packageName) == null) { 567 // Defend against null manifest. 568 if (man != null) { 569 definePackage(packageName, man, urlFactory.getCodeBase(jar)); 570 } else { 571 definePackage(packageName, null, null, null, null, null, null, null); 572 } 573 } 574 } 575 // end patch 576 577 // Because we are doing stream processing, we don't know what 578 // the size of the entries is. So we store them dynamically. 579 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 580 copy(is, baos); 581 582 if (tmp != null) { 583 // Unpack into a temporary working directory which is on the classpath. 584 File file = new File(tmp, entry.getName()); 585 file.getParentFile().mkdirs(); 586 FileOutputStream fos = new FileOutputStream(file); 587 fos.write(baos.toByteArray()); 588 fos.close(); 589 590 } else { 591 // If entry is a class, check to see that it hasn't been defined 592 // already. Class names must be unique within a classloader because 593 // they are cached inside the VM until the classloader is released. 594 if (type.equals("class")) { 595 if (alreadyCached(entryName, jar, baos)) return; 596 byteCode.put(entryName, new ByteCode(entryName, entry.getName(), baos, jar, man)); 597 VERBOSE("cached bytes for class " + entryName); 598 } else { 599 // Another kind of resource. Cache this by name, and also prefixed 600 // by the jar name. Don't duplicate the bytes. This allows us 601 // to map resource lookups to either jar-local, or globally defined. 602 String localname = jar + "/" + entryName; 603 byteCode.put(localname, new ByteCode(localname, entry.getName(), baos, jar, man)); 604 // Keep a set of jar names so we can do multiple-resource lookup by name 605 // as in findResources(). 606 jarNames.add(jar); 607 VERBOSE("cached bytes for local name " + localname); 608 // Only keep the first non-local entry: this is like classpath where the first 609 // to define wins. 610 if (alreadyCached(entryName, jar, baos)) return; 611 612 byteCode.put(entryName, new ByteCode(entryName, entry.getName(), baos, jar, man)); 613 VERBOSE("cached bytes for entry name " + entryName); 614 615 } 616 } 617 } 618 619 /** 620 * Override to ensure that this classloader is the thread context classloader 621 * when used to load a class. Avoids subtle, nasty problems. 622 * 623 */ loadClass(String name, boolean resolve)624 public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { 625 // Set the context classloader in case any classloaders delegate to it. 626 // Otherwise it would default to the sun.misc.Launcher$AppClassLoader which 627 // is used to launch the jar application, and attempts to load through 628 // it would fail if that code is encapsulated inside the one-jar. 629 Thread.currentThread().setContextClassLoader(this); 630 return super.loadClass(name, resolve); 631 } 632 633 /** 634 * Locate the named class in a jar-file, contained inside the 635 * jar file which was used to load <u>this</u> class. 636 */ findClass(String name)637 protected Class findClass(String name) throws ClassNotFoundException { 638 // Delegate to external paths first 639 Class cls = null; 640 if (externalClassLoader != null) { 641 try { 642 return externalClassLoader.loadClass(name); 643 } catch (ClassNotFoundException cnfx) { 644 // continue... 645 } 646 } 647 648 // Make sure not to load duplicate classes. 649 cls = findLoadedClass(name); 650 if (cls != null) return cls; 651 652 // Look up the class in the byte codes. 653 // Translate path? 654 VERBOSE("findClass(" + name + ")"); 655 String cache = name.replace('.', '/') + CLASS; 656 ByteCode bytecode = (ByteCode)byteCode.get(cache); 657 if (bytecode != null) { 658 VERBOSE("found " + name + " in codebase '" + bytecode.codebase + "'"); 659 if (record) { 660 record(bytecode); 661 } 662 // Use a protectionDomain to associate the codebase with the 663 // class. 664 ProtectionDomain pd = (ProtectionDomain)pdCache.get(bytecode.codebase); 665 if (pd == null) { 666 try { 667 URL url = urlFactory.getCodeBase(bytecode.codebase); 668 669 CodeSource source = new CodeSource(url, (Certificate[])null); 670 pd = new ProtectionDomain(source, null, this, null); 671 pdCache.put(bytecode.codebase, pd); 672 } catch (MalformedURLException mux) { 673 throw new ClassNotFoundException(name, mux); 674 } 675 } 676 677 // Do it the simple way. 678 byte bytes[] = bytecode.bytes; 679 680 int i = name.lastIndexOf('.'); 681 if (i != -1) { 682 String pkgname = name.substring(0, i); 683 // Check if package already loaded. 684 Package pkg = getPackage(pkgname); 685 Manifest man = bytecode.manifest; 686 if (pkg != null) { 687 // Package found, so check package sealing. 688 if (pkg.isSealed()) { 689 // Verify that code source URL is the same. 690 if (!pkg.isSealed(pd.getCodeSource().getLocation())) { 691 throw new SecurityException("sealing violation: package " + pkgname + " is sealed"); 692 } 693 694 } else { 695 // Make sure we are not attempting to seal the package 696 // at this code source URL. 697 if ((man != null) && isSealed(pkgname, man)) { 698 throw new SecurityException("sealing violation: can't seal package " + pkgname + ": already loaded"); 699 } 700 } 701 } else { 702 if (man != null) { 703 definePackage(pkgname, man, pd.getCodeSource().getLocation()); 704 } else { 705 definePackage(pkgname, null, null, null, null, null, null, null); 706 } 707 } 708 } 709 710 return defineClass(name, bytes, pd); 711 } 712 VERBOSE(name + " not found"); 713 throw new ClassNotFoundException(name); 714 715 } 716 isSealed(String name, Manifest man)717 private boolean isSealed(String name, Manifest man) { 718 String path = name.concat("/"); 719 Attributes attr = man.getAttributes(path); 720 String sealed = null; 721 if (attr != null) { 722 sealed = attr.getValue(Name.SEALED); 723 } 724 if (sealed == null) { 725 if ((attr = man.getMainAttributes()) != null) { 726 sealed = attr.getValue(Name.SEALED); 727 } 728 } 729 return "true".equalsIgnoreCase(sealed); 730 } 731 732 /** 733 * Defines a new package by name in this ClassLoader. The attributes 734 * contained in the specified Manifest will be used to obtain package 735 * version and sealing information. For sealed packages, the additional URL 736 * specifies the code source URL from which the package was loaded. 737 * 738 * @param name 739 * the package name 740 * @param man 741 * the Manifest containing package version and sealing 742 * information 743 * @param url 744 * the code source url for the package, or null if none 745 * @exception IllegalArgumentException 746 * if the package name duplicates an existing package either 747 * in this class loader or one of its ancestors 748 * @return the newly defined Package object 749 */ definePackage(String name, Manifest man, URL url)750 protected Package definePackage(String name, Manifest man, URL url) throws IllegalArgumentException { 751 String path = name.concat("/"); 752 String specTitle = null, specVersion = null, specVendor = null; 753 String implTitle = null, implVersion = null, implVendor = null; 754 String sealed = null; 755 URL sealBase = null; 756 757 Attributes attr = man.getAttributes(path); 758 if (attr != null) { 759 specTitle = attr.getValue(Name.SPECIFICATION_TITLE); 760 specVersion = attr.getValue(Name.SPECIFICATION_VERSION); 761 specVendor = attr.getValue(Name.SPECIFICATION_VENDOR); 762 implTitle = attr.getValue(Name.IMPLEMENTATION_TITLE); 763 implVersion = attr.getValue(Name.IMPLEMENTATION_VERSION); 764 implVendor = attr.getValue(Name.IMPLEMENTATION_VENDOR); 765 sealed = attr.getValue(Name.SEALED); 766 } 767 attr = man.getMainAttributes(); 768 if (attr != null) { 769 if (specTitle == null) { 770 specTitle = attr.getValue(Name.SPECIFICATION_TITLE); 771 } 772 if (specVersion == null) { 773 specVersion = attr.getValue(Name.SPECIFICATION_VERSION); 774 } 775 if (specVendor == null) { 776 specVendor = attr.getValue(Name.SPECIFICATION_VENDOR); 777 } 778 if (implTitle == null) { 779 implTitle = attr.getValue(Name.IMPLEMENTATION_TITLE); 780 } 781 if (implVersion == null) { 782 implVersion = attr.getValue(Name.IMPLEMENTATION_VERSION); 783 } 784 if (implVendor == null) { 785 implVendor = attr.getValue(Name.IMPLEMENTATION_VENDOR); 786 } 787 if (sealed == null) { 788 sealed = attr.getValue(Name.SEALED); 789 } 790 } 791 if (sealed != null) { 792 boolean isSealed = Boolean.parseBoolean(sealed); 793 if (isSealed) { 794 sealBase = url; 795 } 796 } 797 return definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, sealBase); 798 } 799 defineClass(String name, byte[] bytes, ProtectionDomain pd)800 protected Class defineClass(String name, byte[] bytes, ProtectionDomain pd) throws ClassFormatError { 801 // Simple, non wrapped class definition. 802 VERBOSE("defineClass("+name+")"); 803 return defineClass(name, bytes, 0, bytes.length, pd); 804 } 805 record(ByteCode bytecode)806 protected void record(ByteCode bytecode) { 807 String fileName = bytecode.original; 808 // Write out into the record directory. 809 File dir = new File(recording, flatten? "": bytecode.codebase); 810 File file = new File(dir, fileName); 811 if (!file.exists()) { 812 file.getParentFile().mkdirs(); 813 VERBOSE("" + file); 814 try { 815 FileOutputStream fos = new FileOutputStream(file); 816 fos.write(bytecode.bytes); 817 fos.close(); 818 819 } catch (IOException iox) { 820 System.err.println(PREFIX() + "unable to record " + file + ": " + iox); 821 } 822 823 } 824 } 825 826 /** 827 * Make a path canonical, removing . and .. 828 */ canon(String path)829 protected String canon(String path) { 830 path = path.replaceAll("/\\./", "/"); 831 String canon = path; 832 String next = canon; 833 do { 834 next = canon; 835 canon = canon.replaceFirst("([^/]*/\\.\\./)", ""); 836 } while (!next.equals(canon)); 837 return canon; 838 } 839 /** 840 * Overriden to return resources from the appropriate codebase. 841 * There are basically two ways this method will be called: most commonly 842 * it will be called through the class of an object which wishes to 843 * load a resource, i.e. this.getClass().getResourceAsStream(). Before 844 * passing the call to us, java.lang.Class mangles the name. It 845 * converts a file path such as foo/bar/Class.class into a name like foo.bar.Class, 846 * and it strips leading '/' characters e.g. converting '/foo' to 'foo'. 847 * All of which is a nuisance, since we wish to do a lookup on the original 848 * name of the resource as present in the One-Jar jar files. 849 * The other way is more direct, i.e. this.getClass().getClassLoader().getResourceAsStream(). 850 * Then we get the name unmangled, and can deal with it directly. 851 * 852 * The problem is this: if one resource is called /foo/bar/data, and another 853 * resource is called /foo.bar.data, both will have the same mangled name, 854 * namely 'foo.bar.data' and only one of them will be visible. Perhaps the 855 * best way to deal with this is to store the lookup names in mangled form, and 856 * simply issue warnings if collisions occur. This is not very satisfactory, 857 * but is consistent with the somewhat limiting design of the resource name mapping 858 * strategy in Java today. 859 */ getByteStream(String resource)860 public InputStream getByteStream(String resource) { 861 862 VERBOSE("getByteStream(" + resource + ")"); 863 864 InputStream result = null; 865 if (externalClassLoader != null) { 866 result = externalClassLoader.getResourceAsStream(resource); 867 } 868 869 if (result == null) { 870 // Delegate to parent classloader first. 871 ClassLoader parent = getParent(); 872 if (parent != null) { 873 result = parent.getResourceAsStream(resource); 874 } 875 } 876 877 if (result == null) { 878 // Make resource canonical (remove ., .., etc). 879 resource = canon(resource); 880 881 // Look up resolving first. This allows jar-local 882 // resolution to take place. 883 ByteCode bytecode = (ByteCode)byteCode.get(resolve(resource)); 884 if (bytecode == null) { 885 // Try again with an unresolved name. 886 bytecode = (ByteCode)byteCode.get(resource); 887 } 888 if (bytecode != null) result = new ByteArrayInputStream(bytecode.bytes); 889 } 890 891 // Contributed by SourceForge "ffrog_8" (with thanks, Pierce. T. Wetter III). 892 // Handles JPA loading from jars. 893 if (result == null) { 894 if (jarNames.contains(resource)) { 895 // resource wanted is an actual jar 896 INFO("loading resource file directly" + resource); 897 result = super.getResourceAsStream(resource); 898 } 899 } 900 901 // Special case: if we are a wrapping classloader, look up to our 902 // parent codebase. Logic is that the boot JarLoader will have 903 // delegateToParent = false, the wrapping classloader will have 904 // delegateToParent = true; 905 if (result == null && delegateToParent) { 906 // http://code.google.com/p/onejar-maven-plugin/issues/detail?id=16 907 ClassLoader parentClassLoader = getParent(); 908 909 // JarClassLoader cannot satisfy requests for actual jar files themselves so it must delegate to it's 910 // parent. However, the "parent" is not always a JarClassLoader. 911 if (parentClassLoader instanceof JarClassLoader) { 912 result = ((JarClassLoader)parentClassLoader).getByteStream(resource); 913 } else { 914 result = parentClassLoader.getResourceAsStream(resource); 915 } 916 } 917 VERBOSE("getByteStream(" + resource + ") -> " + result); 918 return result; 919 } 920 921 /** 922 * Resolve a resource name. Look first in jar-relative, then in global scope. 923 * @param resource 924 * @return 925 */ resolve(String $resource)926 protected String resolve(String $resource) { 927 928 if ($resource.startsWith("/")) $resource = $resource.substring(1); 929 930 String resource = null; 931 String caller = getCaller(); 932 ByteCode callerCode = (ByteCode)byteCode.get(caller); 933 934 if (callerCode != null) { 935 // Jar-local first, then global. 936 String tmp = callerCode.codebase + "/" + $resource; 937 if (byteCode.get(tmp) != null) { 938 resource = tmp; 939 } 940 } 941 if (resource == null) { 942 // One last try. 943 if (byteCode.get($resource) == null) { 944 resource = null; 945 } else { 946 resource = $resource; 947 } 948 } 949 VERBOSE("resource " + $resource + " resolved to " + resource + (callerCode != null? " in codebase " + callerCode.codebase: " (unknown codebase)")); 950 return resource; 951 } 952 alreadyCached(String name, String jar, ByteArrayOutputStream baos)953 protected boolean alreadyCached(String name, String jar, ByteArrayOutputStream baos) { 954 // TODO: check resource map to see how we will map requests for this 955 // resource from this jar file. Only a conflict if we are using a 956 // global map and the resource is defined by more than 957 // one jar file (default is to map to local jar). 958 ByteCode existing = (ByteCode)byteCode.get(name); 959 if (existing != null) { 960 byte[] bytes = baos.toByteArray(); 961 // If bytecodes are identical, no real problem. Likewise if it's in 962 // META-INF. 963 if (!Arrays.equals(existing.bytes, bytes) && !name.startsWith("META-INF")) { 964 // TODO: this really needs to be a warning, but there needs to be a way 965 // to shut it down. INFO it for now. Ideally we need to provide a 966 // logging layer (like commons-logging) to allow logging to be delegated. 967 String message = existing.name + " in " + jar + " is hidden by " + existing.codebase + " (with different bytecode)"; 968 if (name.endsWith(".class")) { 969 // This is probably trouble. 970 WARNING(existing.name + " in " + jar + " is hidden by " + existing.codebase + " (with different bytecode)"); 971 } else { 972 INFO(existing.name + " in " + jar + " is hidden by " + existing.codebase + " (with different bytes)"); 973 } 974 } else { 975 VERBOSE(existing.name + " in " + jar + " is hidden by " + existing.codebase + " (with same bytecode)"); 976 } 977 // Speedup GC. 978 bytes = null; 979 return true; 980 } 981 return false; 982 } 983 984 getCaller()985 protected String getCaller() { 986 987 // TODO: revisit caller determination. 988 /* 989 StackTraceElement[] stack = new Throwable().getStackTrace(); 990 // Search upward until we get to a known class, i.e. one with a non-null 991 // codebase. Skip anything in the com.simontuffs.onejar package to avoid 992 // classloader classes. 993 for (int i=0; i<stack.length; i++) { 994 String cls = stack[i].getClassName().replace(".","/") + ".class"; 995 INFO("getCaller(): cls=" + cls); 996 if (byteCode.get(cls) != null) { 997 String caller = stack[i].getClassName(); 998 if (!caller.startsWith("com.simontuffs.onejar")) { 999 return cls; 1000 } 1001 } 1002 } 1003 */ 1004 return null; 1005 } 1006 1007 /** 1008 * Sets the name of the used classes recording directory. 1009 * 1010 * @param $recording A value of "" will use the current working directory 1011 * (not recommended). A value of 'null' will use the default directory, which 1012 * is called 'recording' under the launch directory (recommended). 1013 */ setRecording(String $recording)1014 public void setRecording(String $recording) { 1015 recording = $recording; 1016 if (recording == null) recording = RECORDING; 1017 } 1018 getRecording()1019 public String getRecording() { 1020 return recording; 1021 } 1022 setRecord(boolean $record)1023 public void setRecord(boolean $record) { 1024 record = $record; 1025 } getRecord()1026 public boolean getRecord() { 1027 return record; 1028 } 1029 setFlatten(boolean $flatten)1030 public void setFlatten(boolean $flatten) { 1031 flatten = $flatten; 1032 } isFlatten()1033 public boolean isFlatten() { 1034 return flatten; 1035 } 1036 setVerbose(boolean $verbose)1037 public void setVerbose(boolean $verbose) { 1038 verbose = $verbose; 1039 if (verbose) info = true; 1040 } 1041 getVerbose()1042 public boolean getVerbose() { 1043 return verbose; 1044 } 1045 setInfo(boolean $info)1046 public void setInfo(boolean $info) { 1047 info = $info; 1048 } getInfo()1049 public boolean getInfo() { 1050 return info; 1051 } 1052 setWarning(boolean $warning)1053 public void setWarning(boolean $warning) { 1054 warning = $warning; 1055 } getWarning()1056 public boolean getWarning() { 1057 return warning; 1058 } 1059 protected URLStreamHandler oneJarHandler = new Handler(); 1060 1061 // Injectable URL factory. 1062 public static interface IURLFactory { getURL(String codebase, String resource)1063 public URL getURL(String codebase, String resource) throws MalformedURLException; getCodeBase(String jar)1064 public URL getCodeBase(String jar) throws MalformedURLException; 1065 } 1066 1067 // Resolve URL from codebase and resource. Allow URL factory to be specified by 1068 // user of JarClassLoader. 1069 1070 /** 1071 * FileURLFactory generates URL's which are resolved relative to the filesystem. 1072 * These are compatible with frameworks like Spring, but require knowledge of the 1073 * location of the one-jar file via Boot.getMyJarPath(). 1074 */ 1075 public static class FileURLFactory implements IURLFactory { 1076 public URLStreamHandler jarHandler = new URLStreamHandler() { 1077 protected URLConnection openConnection(URL url) throws IOException { 1078 URLConnection connection = new OneJarURLConnection(url); 1079 connection.connect(); 1080 return connection; 1081 } 1082 }; 1083 // TODO: Unify getURL and getCodeBase, if possible. getURL(String codebase, String resource)1084 public URL getURL(String codebase, String resource) throws MalformedURLException { 1085 if (!codebase.equals("/")) { 1086 codebase = codebase + "!/"; 1087 } else { 1088 codebase = ""; 1089 } 1090 String path = "file:/" + Boot.getMyJarPath() + "!/" + codebase + resource; 1091 URL url = new URL("jar", "", -1, path, jarHandler); 1092 return url; 1093 } getCodeBase(String jar)1094 public URL getCodeBase(String jar) throws MalformedURLException { 1095 ProtectionDomain cd = JarClassLoader.class.getProtectionDomain(); 1096 URL url = cd.getCodeSource().getLocation(); 1097 if (url != null) { 1098 url = new URL("jar", "", -1, url + "!/" + jar, jarHandler); 1099 } 1100 return url; 1101 } 1102 } 1103 1104 /** 1105 * OneJarURLFactory generates URL's which are efficient, using the in-memory bytecode 1106 * to access the resources. 1107 * @author simon 1108 * 1109 */ 1110 public static class OneJarURLFactory implements IURLFactory { getURL(String codebase, String resource)1111 public URL getURL(String codebase, String resource) throws MalformedURLException { 1112 String base = resource.endsWith(".class")? "": codebase + "/"; 1113 URL url = new URL(Handler.PROTOCOL + ":/" + base + resource); 1114 return url; 1115 } getCodeBase(String jar)1116 public URL getCodeBase(String jar) throws MalformedURLException { 1117 return new URL(Handler.PROTOCOL + ":" + jar); 1118 } 1119 } 1120 getResource(String name)1121 public URL getResource(String name) { 1122 // Delegate to external first. 1123 if (externalClassLoader != null) { 1124 URL url = externalClassLoader.getResource(name); 1125 if (url != null) 1126 return url; 1127 } 1128 return super.getResource(name); 1129 } 1130 1131 protected IURLFactory urlFactory = new FileURLFactory(); 1132 1133 // Allow override for urlFactory setURLFactory(String urlFactory)1134 public void setURLFactory(String urlFactory) throws ClassNotFoundException, IllegalAccessException, InstantiationException { 1135 this.urlFactory = (IURLFactory)loadClass(urlFactory).newInstance(); 1136 } 1137 getURLFactory()1138 public IURLFactory getURLFactory() { 1139 return urlFactory; 1140 } 1141 1142 /* (non-Javadoc) 1143 * @see java.lang.ClassLoader#findResource(java.lang.String) 1144 */ 1145 // TODO: Revisit the issue of protocol handlers for findResource() 1146 // and findResources(); findResource(String $resource)1147 protected URL findResource(String $resource) { 1148 try { 1149 VERBOSE("findResource(\"" + $resource + "\")"); 1150 URL url = externalClassLoader!=null ? externalClassLoader.getResource($resource) : null; 1151 if (url != null) 1152 { 1153 INFO("findResource() found in external: \"" + $resource + "\""); 1154 //VERBOSE("findResource(): " + $resource + "=" + url); 1155 return url; 1156 } 1157 // Delegate to parent. 1158 ClassLoader parent = getParent(); 1159 if (parent != null) { 1160 url = parent.getResource($resource); 1161 if (url != null) { 1162 return url; 1163 } 1164 } 1165 // Do we have the named resource in our cache? If so, construct a 1166 // 'onejar:' URL so that a later attempt to access the resource 1167 // will be redirected to our Handler class, and thence to this class. 1168 String resource = resolve($resource); 1169 if (resource != null) { 1170 // We know how to handle it. 1171 ByteCode entry = ((ByteCode) byteCode.get(resource)); 1172 INFO("findResource() found: \"" + $resource + "\" for caller " + getCaller() + " in codebase " + entry.codebase); 1173 return urlFactory.getURL(entry.codebase, $resource); 1174 } 1175 INFO("findResource(): unable to locate \"" + $resource + "\""); 1176 // If all else fails, return null. 1177 return null; 1178 } catch (MalformedURLException mux) { 1179 WARNING("unable to locate " + $resource + " due to " + mux); 1180 } 1181 return null; 1182 1183 } 1184 findResources(String name)1185 protected Enumeration findResources(String name) throws IOException { 1186 INFO("findResources(" + name + ")"); 1187 INFO("findResources: looking in " + jarNames); 1188 Iterator iter = jarNames.iterator(); 1189 final List resources = new ArrayList(); 1190 while (iter.hasNext()) { 1191 String resource = iter.next().toString() + "/" + name; 1192 ByteCode entry = ((ByteCode) byteCode.get(resource)); 1193 if (byteCode.containsKey(resource)) { 1194 URL url = urlFactory.getURL(entry.codebase, name); 1195 INFO("findResources(): Adding " + url + " to resources list."); 1196 resources.add(url); 1197 } 1198 } 1199 final Iterator ri = resources.iterator(); 1200 return new Enumeration() { 1201 public boolean hasMoreElements() { 1202 return ri.hasNext(); 1203 } 1204 public Object nextElement() { 1205 return ri.next(); 1206 } 1207 }; 1208 } 1209 1210 /** 1211 * Utility to assist with copying InputStream to OutputStream. All 1212 * bytes are copied, but both streams are left open. 1213 * @param in Source of bytes to copy. 1214 * @param out Destination of bytes to copy. 1215 * @throws IOException 1216 */ 1217 protected void copy(InputStream in, OutputStream out) throws IOException { 1218 byte[] buf = new byte[1024]; 1219 while (true) { 1220 int len = in.read(buf); 1221 if (len < 0) break; 1222 out.write(buf, 0, len); 1223 } 1224 } 1225 1226 public String toString() { 1227 return super.toString() + (name != null? "(" + name + ")": ""); 1228 } 1229 1230 /** 1231 * Returns name of the classloader. 1232 * @return 1233 */ 1234 public String getName() { 1235 return name; 1236 } 1237 1238 /** 1239 * Sets name of the classloader. Default is null. 1240 * @param string 1241 */ 1242 public void setName(String string) { 1243 name = string; 1244 } 1245 1246 public void setExpand(boolean expand) { 1247 noExpand = !expand; 1248 } 1249 1250 public boolean isExpanded() { 1251 return expanded; 1252 } 1253 1254 /** 1255 * Preloader for {@link JarClassLoader#findTheLibrary(String, String)} to allow arch-specific native libraries 1256 * 1257 * @param name the (system specific) name of the requested library 1258 * @author Sebastian Just 1259 */ 1260 protected String findLibrary(String name) { 1261 final String os = System.getProperty("os.name").toLowerCase(); 1262 final String arch = System.getProperty("os.arch").toLowerCase(); 1263 1264 final String BINLIB_LINUX32_PREFIX = BINLIB_PREFIX + "linux32/"; 1265 final String BINLIB_LINUX64_PREFIX = BINLIB_PREFIX + "linux64/"; 1266 final String BINLIB_MACOSX_PREFIX = BINLIB_PREFIX + "macosx/"; 1267 final String BINLIB_WINDOWS32_PREFIX = BINLIB_PREFIX + "windows32/"; 1268 final String BINLIB_WINDOWS64_PREFIX = BINLIB_PREFIX + "windows64/"; 1269 1270 String binlib = null; 1271 1272 // Mac 1273 if (os.startsWith("mac os x")) { 1274 //TODO Nood arch detection on mac 1275 binlib = BINLIB_MACOSX_PREFIX; 1276 // Windows 1277 } else if (os.startsWith("windows")) { 1278 if (arch.equals("x86")) { 1279 binlib = BINLIB_WINDOWS32_PREFIX; 1280 } else { 1281 binlib = BINLIB_WINDOWS64_PREFIX; 1282 } 1283 // So it have to be Linux 1284 } else { 1285 if (arch.equals("i386")) { 1286 binlib = BINLIB_LINUX32_PREFIX; 1287 } else { 1288 binlib = BINLIB_LINUX64_PREFIX; 1289 } 1290 }//TODO Need some work for solaris 1291 1292 VERBOSE("Using arch-specific native library path: " + binlib); 1293 1294 String retValue = findTheLibrary(binlib, name); 1295 if (retValue != null) { 1296 VERBOSE("Found in arch-specific directory!"); 1297 return retValue; 1298 } else { 1299 VERBOSE("Search in standard native directory!"); 1300 return findTheLibrary(BINLIB_PREFIX, name); 1301 } 1302 } 1303 1304 /** 1305 * If the system specific library exists in the JAR, expand it and return the path 1306 * to the expanded library to the caller. Otherwise return null so the caller 1307 * searches the java.library.path for the requested library. 1308 * 1309 * 1310 * @author Christopher Ottley 1311 * @param name the (system specific) name of the requested library 1312 * @param BINLIB_PREFIX the (system specific) folder to search in 1313 * @return the full pathname to the requested library, or null 1314 * @see Runtime#loadLibrary() 1315 * @since 1.2 1316 */ 1317 protected String findTheLibrary(String BINLIB_PREFIX, String name) { 1318 String result = null; // By default, search the java.library.path for it 1319 1320 String resourcePath = BINLIB_PREFIX + System.mapLibraryName(name); 1321 1322 // If it isn't in the map, try to expand to temp and return the full path 1323 // otherwise, remain null so the java.library.path is searched. 1324 1325 // If it has been expanded already and in the map, return the expanded value 1326 if (binLibPath.get(resourcePath) != null) { 1327 result = (String)binLibPath.get(resourcePath); 1328 } else { 1329 1330 // See if it's a resource in the JAR that can be extracted 1331 File tempNativeLib = null; 1332 FileOutputStream os = null; 1333 try { 1334 int lastdot = resourcePath.lastIndexOf('.'); 1335 String suffix = null; 1336 if (lastdot >= 0) { 1337 suffix = resourcePath.substring(lastdot); 1338 } 1339 InputStream is = this.getClass().getResourceAsStream("/" + resourcePath); 1340 1341 if ( is != null ) { 1342 tempNativeLib = File.createTempFile(name + "-", suffix); 1343 tempNativeLib.deleteOnExit(); 1344 os = new FileOutputStream(tempNativeLib); 1345 copy(is, os); 1346 os.close(); 1347 VERBOSE("Stored native library " + name + " at " + tempNativeLib); 1348 result = tempNativeLib.getPath(); 1349 binLibPath.put(resourcePath, result); 1350 } else { 1351 // Library is not in the jar 1352 // Return null by default to search the java.library.path 1353 VERBOSE("No native library at " + resourcePath + 1354 "java.library.path will be searched instead."); 1355 } 1356 } catch(Throwable e) { 1357 // Couldn't load the library 1358 // Return null by default to search the java.library.path 1359 WARNING("Unable to load native library: " + e); 1360 } 1361 1362 } 1363 1364 return result; 1365 } 1366 1367 protected String getConfirmation(File location) throws IOException { 1368 String answer = ""; 1369 while (answer == null || (!answer.startsWith("n") && !answer.startsWith("y") && !answer.startsWith("q"))) { 1370 promptForConfirm(location); 1371 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 1372 answer = br.readLine(); 1373 br.close(); 1374 } 1375 return answer; 1376 } 1377 1378 protected void promptForConfirm(File location) { 1379 PRINTLN("Do you want to allow '" + Boot.getMyJarName() + "' to expand files into the file-system at the following location?"); 1380 PRINTLN(" " + location); 1381 PRINT("Answer y(es) to expand files, n(o) to continue without expanding, or q(uit) to exit: "); 1382 } 1383 1384 } 1385