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