1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */ 
17  
18  package org.apache.commons.logging;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.net.URL;
24  import java.net.URLClassLoader;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.Enumeration;
28  import java.util.HashMap;
29  import java.util.Iterator;
30  import java.util.Map;
31  
32  /**
33   * A ClassLoader which sees only specified classes, and which can be
34   * set to do parent-first or child-first path lookup.
35   * <p>
36   * Note that this classloader is not "industrial strength"; users
37   * looking for such a class may wish to look at the Tomcat sourcecode
38   * instead. In particular, this class may not be threadsafe.
39   * <p>
40   * Note that the ClassLoader.getResources method isn't overloaded here.
41   * It would be nice to ensure that when child-first lookup is set the
42   * resources from the child are returned earlier in the list than the
43   * resources from the parent. However overriding this method isn't possible
44   * as the java 1.4 version of ClassLoader declares this method final
45   * (though the java 1.5 version has removed the final qualifier). As the
46   * ClassLoader javadoc doesn't specify the order in which resources
47   * are returned, it's valid to return the resources in any order (just
48   * untidy) so the inherited implementation is technically ok.
49   */
50  
51  public class PathableClassLoader extends URLClassLoader {
52      
53      private static final URL[] NO_URLS = new URL[0];
54      
55      /**
56       * A map of package-prefix to ClassLoader. Any class which is in
57       * this map is looked up via the specified classloader instead of
58       * the classpath associated with this classloader or its parents.
59       * <p>
60       * This is necessary in order for the rest of the world to communicate
61       * with classes loaded via a custom classloader. As an example, junit
62       * testcases which are loaded via a custom classloader needs to see
63       * the same junit classes as the code invoking the testcase, otherwise
64       * they can't pass result objects back. 
65       * <p>
66       * Normally, only a classloader created with a null parent needs to
67       * have any lookasides defined.
68       */
69      private HashMap lookasides = null;
70  
71      /**
72       * See setParentFirst.
73       */
74      private boolean parentFirst = true;
75  
76      /**
77       * Constructor.
78       * <p>
79       * Often, null is passed as the parent, ie the parent of the new
80       * instance is the bootloader. This ensures that the classpath is
81       * totally clean; nothing but the standard java library will be
82       * present.
83       * <p>
84       * When using a null parent classloader with a junit testcase, it *is*
85       * necessary for the junit library to also be visible. In this case, it
86       * is recommended that the following code be used:
87       * <pre>
88       * pathableLoader.useExplicitLoader(
89       *   "junit.",
90       *   junit.framework.Test.class.getClassLoader());
91       * </pre>
92       * Note that this works regardless of whether junit is on the system
93       * classpath, or whether it has been loaded by some test framework that
94       * creates its own classloader to run unit tests in (eg maven2's
95       * Surefire plugin).
96       */
97      public PathableClassLoader(ClassLoader parent) {
98          super(NO_URLS, parent);
99      }
100     
101     /**
102      * Allow caller to explicitly add paths. Generally this not a good idea;
103      * use addLogicalLib instead, then define the location for that logical
104      * library in the build.xml file.
105      */
106     public void addURL(URL url) {
107         super.addURL(url);
108     }
109 
110     /**
111      * Specify whether this classloader should ask the parent classloader
112      * to resolve a class first, before trying to resolve it via its own
113      * classpath.
114      * <p> 
115      * Checking with the parent first is the normal approach for java, but
116      * components within containers such as servlet engines can use 
117      * child-first lookup instead, to allow the components to override libs
118      * which are visible in shared classloaders provided by the container.
119      * <p>
120      * Note that the method getResources always behaves as if parentFirst=true,
121      * because of limitations in java 1.4; see the javadoc for method
122      * getResourcesInOrder for details.
123      * <p>
124      * This value defaults to true.
125      */
126     public void setParentFirst(boolean state) {
127         parentFirst = state;
128     }
129 
130     /**
131      * For classes with the specified prefix, get them from the system
132      * classpath <i>which is active at the point this method is called</i>.
133      * <p>
134      * This method is just a shortcut for
135      * <pre>
136      * useExplicitLoader(prefix, ClassLoader.getSystemClassLoader());
137      * </pre>
138      * <p>
139      * Of course, this assumes that the classes of interest are already
140      * in the classpath of the system classloader.
141      */
142     public void useSystemLoader(String prefix) {
143         useExplicitLoader(prefix, ClassLoader.getSystemClassLoader());
144         
145     }
146 
147     /**
148      * Specify a classloader to use for specific java packages.
149      * <p>
150      * The specified classloader is normally a loader that is NOT
151      * an ancestor of this classloader. In particular, this loader
152      * may have the bootloader as its parent, but be configured to 
153      * see specific other classes (eg the junit library loaded
154      * via the system classloader).
155      * <p>
156      * The differences between using this method, and using
157      * addLogicalLib are:
158      * <ul>
159      * <li>If code calls getClassLoader on a class loaded via
160      * "lookaside", then traces up its inheritance chain, it
161      * will see the "real" classloaders. When the class is remapped
162      * into this classloader via addLogicalLib, the classloader
163      * chain seen is this object plus ancestors.
164      * <li>If two different jars contain classes in the same
165      * package, then it is not possible to load both jars into
166      * the same "lookaside" classloader (eg the system classloader)
167      * then map one of those subsets from here. Of course they could
168      * be loaded into two different "lookaside" classloaders and
169      * then a prefix used to map from here to one of those classloaders.
170      * </ul>
171      */
172     public void useExplicitLoader(String prefix, ClassLoader loader) {
173         if (lookasides == null) {
174             lookasides = new HashMap();
175         }
176         lookasides.put(prefix, loader);
177     }
178 
179     /**
180      * Specify a collection of logical libraries. See addLogicalLib.
181      */
182     public void addLogicalLib(String[] logicalLibs) {
183         for(int i=0; i<logicalLibs.length; ++i) {
184             addLogicalLib(logicalLibs[i]);
185         }
186     }
187 
188     /**
189      * Specify a logical library to be included in the classpath used to
190      * locate classes. 
191      * <p>
192      * The specified lib name is used as a key into the system properties;
193      * there is expected to be a system property defined with that name
194      * whose value is a url that indicates where that logical library can
195      * be found. Typically this is the name of a jar file, or a directory
196      * containing class files.
197      * <p>
198      * If there is no system property, but the classloader that loaded
199      * this class is a URLClassLoader then the set of URLs that the
200      * classloader uses for its classpath is scanned; any jar in the
201      * URL set whose name starts with the specified string is added to
202      * the classpath managed by this instance. 
203      * <p>
204      * Using logical library names allows the calling code to specify its
205      * desired classpath without knowing the exact location of the necessary
206      * classes. 
207      */
208     public void addLogicalLib(String logicalLib) {
209         // first, check the system properties
210         String filename = System.getProperty(logicalLib);
211         if (filename != null) {
212             try {
213                 URL libUrl = new File(filename).toURL();
214                 addURL(libUrl);
215                 return;
216             } catch(java.net.MalformedURLException e) {
217                 throw new UnknownError(
218                     "Invalid file [" + filename + "] for logical lib [" + logicalLib + "]");
219             }
220         }
221 
222         // now check the classpath for a similar-named lib
223         URL libUrl = libFromClasspath(logicalLib);
224         if (libUrl != null) {
225             addURL(libUrl);
226             return;
227         }
228 
229         // lib not found
230         throw new UnknownError(
231             "Logical lib [" + logicalLib + "] is not defined"
232             + " as a System property.");
233     }
234 
235     /**
236      * If the classloader that loaded this class has this logical lib in its
237      * path, then return the matching URL otherwise return null.
238      * <p>
239      * This only works when the classloader loading this class is an instance
240      * of URLClassLoader and thus has a getURLs method that returns the classpath
241      * it uses when loading classes. However in practice, the vast majority of the
242      * time this type is the classloader used.
243      * <p>
244      * The classpath of the classloader for this instance is scanned, and any
245      * jarfile in the path whose name starts with the logicalLib string is
246      * considered a match. For example, passing "foo" will match a url
247      * of <code>file:///some/where/foo-2.7.jar</code>.
248      * <p>
249      * When multiple classpath entries match the specified logicalLib string,
250      * the one with the shortest filename component is returned. This means that
251      * if "foo-1.1.jar" and "foobar-1.1.jar" are in the path, then a logicalLib
252      * name of "foo" will match the first entry above.
253      */
254     private URL libFromClasspath(String logicalLib) {
255         ClassLoader cl = this.getClass().getClassLoader();
256         if (cl instanceof URLClassLoader == false) {
257             return null;
258         }
259         
260         URLClassLoader ucl = (URLClassLoader) cl;
261         URL[] path = ucl.getURLs();
262         URL shortestMatch = null;
263         int shortestMatchLen = Integer.MAX_VALUE;
264         for(int i=0; i<path.length; ++i) {
265             URL u = path[i];
266             
267             // extract the filename bit on the end of the url
268             String filename = u.toString();
269             if (!filename.endsWith(".jar")) {
270                 // not a jarfile, ignore it
271                 continue;
272             }
273 
274             int lastSlash = filename.lastIndexOf('/');
275             if (lastSlash >= 0) {
276                 filename = filename.substring(lastSlash+1);
277             }
278             
279             if (filename.startsWith(logicalLib)) {
280                 // ok, this is a candidate
281                 if (filename.length() < shortestMatchLen) {
282                     shortestMatch = u;
283                     shortestMatchLen = filename.length();
284                 }
285             }
286         }
287         
288         return shortestMatch;
289     }
290 
291     /**
292      * Override ClassLoader method.
293      * <p>
294      * For each explicitly mapped package prefix, if the name matches the 
295      * prefix associated with that entry then attempt to load the class via 
296      * that entries' classloader.
297      */
298     protected Class loadClass(String name, boolean resolve) 
299     throws ClassNotFoundException {
300         // just for performance, check java and javax
301         if (name.startsWith("java.") || name.startsWith("javax.")) {
302             return super.loadClass(name, resolve);
303         }
304 
305         if (lookasides != null) {
306             for(Iterator i = lookasides.entrySet().iterator(); i.hasNext(); ) {
307                 Map.Entry entry = (Map.Entry) i.next();
308                 String prefix = (String) entry.getKey();
309                 if (name.startsWith(prefix) == true) {
310                     ClassLoader loader = (ClassLoader) entry.getValue();
311                     Class clazz = Class.forName(name, resolve, loader);
312                     return clazz;
313                 }
314             }
315         }
316         
317         if (parentFirst) {
318             return super.loadClass(name, resolve);
319         } else {
320             // Implement child-first. 
321             //
322             // It appears that the findClass method doesn't check whether the
323             // class has already been loaded. This seems odd to me, but without
324             // first checking via findLoadedClass we can get java.lang.LinkageError
325             // with message "duplicate class definition" which isn't good.
326             
327             try {
328                 Class clazz = findLoadedClass(name);
329                 if (clazz == null) {
330                     clazz = super.findClass(name);
331                 }
332                 if (resolve) {
333                     resolveClass(clazz);
334                 }
335                 return clazz;
336             } catch(ClassNotFoundException e) {
337                 return super.loadClass(name, resolve);
338             }
339         }
340     }
341     
342     /**
343      * Same as parent class method except that when parentFirst is false
344      * the resource is looked for in the local classpath before the parent
345      * loader is consulted.
346      */
347     public URL getResource(String name) {
348         if (parentFirst) {
349             return super.getResource(name);
350         } else {
351             URL local = super.findResource(name);
352             if (local != null) {
353                 return local;
354             }
355             return super.getResource(name);
356         }
357     }
358     
359     /**
360      * Emulate a proper implementation of getResources which respects the
361      * setting for parentFirst.
362      * <p>
363      * Note that it's not possible to override the inherited getResources, as
364      * it's declared final in java1.4 (thought that's been removed for 1.5).
365      * The inherited implementation always behaves as if parentFirst=true.
366      */
367     public Enumeration getResourcesInOrder(String name) throws IOException {
368         if (parentFirst) {
369             return super.getResources(name);
370         } else {
371             Enumeration localUrls = super.findResources(name);
372             
373             ClassLoader parent = getParent();
374             if (parent == null) {
375                 // Alas, there is no method to get matching resources
376                 // from a null (BOOT) parent classloader. Calling
377                 // ClassLoader.getSystemClassLoader isn't right. Maybe
378                 // calling Class.class.getResources(name) would do?
379                 //
380                 // However for the purposes of unit tests, we can
381                 // simply assume that no relevant resources are
382                 // loadable from the parent; unit tests will never be
383                 // putting any of their resources in a "boot" classloader
384                 // path!
385                 return localUrls;
386             }
387             Enumeration parentUrls = parent.getResources(name);
388 
389             ArrayList localItems = toList(localUrls);
390             ArrayList parentItems = toList(parentUrls);
391             localItems.addAll(parentItems);
392             return Collections.enumeration(localItems);
393         }
394     }
395     
396     /**
397      * 
398      * Clean implementation of list function of 
399      * {@link java.utils.Collection} added in JDK 1.4 
400      * @param en <code>Enumeration</code>, possibly null
401      * @return <code>ArrayList</code> containing the enumerated
402      * elements in the enumerated order, not null
403      */
404     private ArrayList toList(Enumeration en) {
405         ArrayList results = new ArrayList();
406         if (en != null) {
407             while (en.hasMoreElements()){
408                 Object element = en.nextElement();
409                 results.add(element);
410             }
411         }
412         return results;
413     }
414     
415     /**
416      * Same as parent class method except that when parentFirst is false
417      * the resource is looked for in the local classpath before the parent
418      * loader is consulted.
419      */
420     public InputStream getResourceAsStream(String name) {
421         if (parentFirst) {
422             return super.getResourceAsStream(name);
423         } else {
424             URL local = super.findResource(name);
425             if (local != null) {
426                 try {
427                     return local.openStream();
428                 } catch(IOException e) {
429                     // TODO: check if this is right or whether we should
430                     // fall back to trying parent. The javadoc doesn't say...
431                     return null;
432                 }
433             }
434             return super.getResourceAsStream(name);
435         }
436     }
437 }