View Javadoc
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  package org.apache.commons.logging;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.net.URL;
23  import java.net.URLClassLoader;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.Enumeration;
27  import java.util.HashMap;
28  import java.util.Map;
29  
30  import junit.framework.Assert;
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 class loader 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 = {};
54  
55      /**
56       * A map of package-prefix to ClassLoader. Any class which is in
57       * this map is looked up via the specified class loader instead of
58       * the classpath associated with this class loader 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 class loader. As an example, junit
62       * tests which are loaded via a custom class loader needs to see
63       * the same junit classes as the code invoking the test, otherwise
64       * they can't pass result objects back.
65       * <p>
66       * Normally, only a class loader created with a null parent needs to
67       * have any lookasides defined.
68       */
69      private HashMap<String, ClassLoader> lookasides;
70  
71      /**
72       * See setParentFirst.
73       */
74      private boolean parentFirst = true;
75  
76      /**
77       * Constructs a new instance.
78       * <p>
79       * Often, null is passed as the parent, that is, 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 class loader with a junit test, 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 class loader to run unit tests in (eg maven2's
95       * Surefire plugin).
96       */
97      public PathableClassLoader(final ClassLoader parent) {
98          super(NO_URLS, parent);
99      }
100 
101     /**
102      * Specify a logical library to be included in the classpath used to
103      * locate classes.
104      * <p>
105      * The specified lib name is used as a key into the system properties;
106      * there is expected to be a system property defined with that name
107      * whose value is a url that indicates where that logical library can
108      * be found. Typically this is the name of a jar file, or a directory
109      * containing class files.
110      * <p>
111      * If there is no system property, but the class loader that loaded
112      * this class is a URLClassLoader then the set of URLs that the
113      * class loader uses for its classpath is scanned; any jar in the
114      * URL set whose name starts with the specified string is added to
115      * the classpath managed by this instance.
116      * <p>
117      * Using logical library names allows the calling code to specify its
118      * desired classpath without knowing the exact location of the necessary
119      * classes.
120      */
121     public void addLogicalLib(final String logicalLib) {
122         // first, check the system properties
123         final String fileName = System.getProperty(logicalLib);
124         if (fileName != null) {
125             try {
126                 final File file = new File(fileName);
127                 if (!file.exists()) {
128                     Assert.fail("Unable to add logical library " + fileName);
129                 }
130                 addURL(file.toURL());
131                 return;
132             } catch (final java.net.MalformedURLException e) {
133                 throw new UnknownError(
134                     "Invalid file [" + fileName + "] for logical lib [" + logicalLib + "]");
135             }
136         }
137 
138         // now check the classpath for a similar-named lib
139         final URL libUrl = libFromClasspath(logicalLib);
140         if (libUrl != null) {
141             addURL(libUrl);
142             return;
143         }
144 
145         // lib not found
146         throw new UnknownError(
147             "Logical lib [" + logicalLib + "] is not defined"
148             + " as a System property.");
149     }
150 
151     /**
152      * Specify a collection of logical libraries. See addLogicalLib.
153      */
154     public void addLogicalLib(final String[] logicalLibs) {
155         for (final String logicalLib : logicalLibs) {
156             addLogicalLib(logicalLib);
157         }
158     }
159 
160     /**
161      * Allow caller to explicitly add paths. Generally this not a good idea;
162      * use addLogicalLib instead, then define the location for that logical
163      * library in the build.xml file.
164      */
165     @Override
166     public void addURL(final URL url) {
167         super.addURL(url);
168     }
169 
170     /**
171      * Same as parent class method except that when parentFirst is false
172      * the resource is looked for in the local classpath before the parent
173      * loader is consulted.
174      */
175     @Override
176     public URL getResource(final String name) {
177         if (parentFirst) {
178             return super.getResource(name);
179         }
180         final URL local = super.findResource(name);
181         if (local != null) {
182             return local;
183         }
184         return super.getResource(name);
185     }
186 
187     /**
188      * Same as parent class method except that when parentFirst is false
189      * the resource is looked for in the local classpath before the parent
190      * loader is consulted.
191      */
192     @Override
193     public InputStream getResourceAsStream(final String name) {
194         if (parentFirst) {
195             return super.getResourceAsStream(name);
196         }
197         final URL local = super.findResource(name);
198         if (local != null) {
199             try {
200                 return local.openStream();
201             } catch (final IOException e) {
202                 // TODO: check if this is right or whether we should
203                 // fall back to trying parent. The Javadoc doesn't say...
204                 return null;
205             }
206         }
207         return super.getResourceAsStream(name);
208     }
209 
210     /**
211      * Emulate a proper implementation of getResources which respects the
212      * setting for parentFirst.
213      * <p>
214      * Note that it's not possible to override the inherited getResources, as
215      * it's declared final in java1.4 (thought that's been removed for 1.5).
216      * The inherited implementation always behaves as if parentFirst=true.
217      */
218     public Enumeration<URL> getResourcesInOrder(final String name) throws IOException {
219         if (parentFirst) {
220             return super.getResources(name);
221         }
222         final Enumeration<URL> localUrls = super.findResources(name);
223         final ClassLoader parent = getParent();
224         if (parent == null) {
225             // Alas, there is no method to get matching resources
226             // from a null (BOOT) parent class loader. Calling
227             // ClassLoader.getSystemClassLoader isn't right. Maybe
228             // calling Class.class.getResources(name) would do?
229             //
230             // However for the purposes of unit tests, we can
231             // simply assume that no relevant resources are
232             // loadable from the parent; unit tests will never be
233             // putting any of their resources in a "boot" class loader
234             // path!
235             return localUrls;
236         }
237         final Enumeration<URL> parentUrls = parent.getResources(name);
238         final ArrayList<URL> localItems = toList(localUrls);
239         final ArrayList<URL> parentItems = toList(parentUrls);
240         localItems.addAll(parentItems);
241         return Collections.enumeration(localItems);
242     }
243 
244     /**
245      * If the class loader that loaded this class has this logical lib in its
246      * path, then return the matching URL otherwise return null.
247      * <p>
248      * This only works when the class loader loading this class is an instance
249      * of URLClassLoader and thus has a getURLs method that returns the classpath
250      * it uses when loading classes. However in practice, the vast majority of the
251      * time this type is the class loader used.
252      * <p>
253      * The classpath of the class loader for this instance is scanned, and any
254      * jarfile in the path whose name starts with the logicalLib string is
255      * considered a match. For example, passing "foo" will match a url
256      * of {@code file:///some/where/foo-2.7.jar}.
257      * <p>
258      * When multiple classpath entries match the specified logicalLib string,
259      * the one with the shortest file name component is returned. This means that
260      * if "foo-1.1.jar" and "foobar-1.1.jar" are in the path, then a logicalLib
261      * name of "foo" will match the first entry above.
262      */
263     private URL libFromClasspath(final String logicalLib) {
264         final ClassLoader cl = this.getClass().getClassLoader();
265         if (!(cl instanceof URLClassLoader)) {
266             return null;
267         }
268 
269         final URLClassLoader ucl = (URLClassLoader) cl;
270         final URL[] path = ucl.getURLs();
271         URL shortestMatch = null;
272         int shortestMatchLen = Integer.MAX_VALUE;
273         for (final URL u : path) {
274             // extract the file name bit on the end of the URL
275             String fileName = u.toString();
276             if (!fileName.endsWith(".jar")) {
277                 // not a jarfile, ignore it
278                 continue;
279             }
280 
281             final int lastSlash = fileName.lastIndexOf('/');
282             if (lastSlash >= 0) {
283                 fileName = fileName.substring(lastSlash+1);
284             }
285 
286             // ok, this is a candidate
287             if (fileName.startsWith(logicalLib) && fileName.length() < shortestMatchLen) {
288                 shortestMatch = u;
289                 shortestMatchLen = fileName.length();
290             }
291         }
292 
293         return shortestMatch;
294     }
295 
296     /**
297      * Override ClassLoader method.
298      * <p>
299      * For each explicitly mapped package prefix, if the name matches the
300      * prefix associated with that entry then attempt to load the class via
301      * that entries' class loader.
302      */
303     @Override
304     protected Class<?> loadClass(final String name, final boolean resolve)
305     throws ClassNotFoundException {
306         // just for performance, check java and javax
307         if (name.startsWith("java.") || name.startsWith("javax.")) {
308             return super.loadClass(name, resolve);
309         }
310 
311         if (lookasides != null) {
312             for (final Map.Entry<String, ClassLoader> entry : lookasides.entrySet()) {
313                 final String prefix = entry.getKey();
314                 if (name.startsWith(prefix)) {
315                     final ClassLoader loader = entry.getValue();
316                     return Class.forName(name, resolve, loader);
317                 }
318             }
319         }
320 
321         if (parentFirst) {
322             return super.loadClass(name, resolve);
323         }
324         try {
325             Class<?> clazz = findLoadedClass(name);
326             if (clazz == null) {
327                 clazz = super.findClass(name);
328             }
329             if (resolve) {
330                 resolveClass(clazz);
331             }
332             return clazz;
333         } catch (final ClassNotFoundException e) {
334             return super.loadClass(name, resolve);
335         }
336     }
337 
338     /**
339      * Specify whether this class loader should ask the parent class loader
340      * to resolve a class first, before trying to resolve it via its own
341      * classpath.
342      * <p>
343      * Checking with the parent first is the normal approach for java, but
344      * components within containers such as servlet engines can use
345      * child-first lookup instead, to allow the components to override libs
346      * which are visible in shared class loaders provided by the container.
347      * <p>
348      * Note that the method getResources always behaves as if parentFirst=true,
349      * because of limitations in Java 1.4; see the Javadoc for method
350      * getResourcesInOrder for details.
351      * <p>
352      * This value defaults to true.
353      */
354     public void setParentFirst(final boolean state) {
355         parentFirst = state;
356     }
357 
358     /**
359      * Clean implementation of list function of
360      * {@link java.util.Collection} added in JDK 1.4
361      * @param en {@code Enumeration}, possibly null
362      * @return {@code ArrayList} containing the enumerated
363      * elements in the enumerated order, not null
364      */
365     private <E> ArrayList<E> toList(final Enumeration<E> en) {
366         final ArrayList<E> results = new ArrayList<>();
367         if (en != null) {
368             while (en.hasMoreElements()) {
369                 results.add(en.nextElement());
370             }
371         }
372         return results;
373     }
374 
375     /**
376      * Specify a class loader to use for specific Java packages.
377      * <p>
378      * The specified class loader is normally a loader that is NOT
379      * an ancestor of this class loader. In particular, this loader
380      * may have the bootloader as its parent, but be configured to
381      * see specific other classes (eg the junit library loaded
382      * via the system class loader).
383      * <p>
384      * The differences between using this method, and using
385      * addLogicalLib are:
386      * <ul>
387      * <li>If code calls getClassLoader on a class loaded via
388      * "lookaside", then traces up its inheritance chain, it
389      * will see the "real" class loaders. When the class is remapped
390      * into this class loader via addLogicalLib, the class loader
391      * chain seen is this object plus ancestors.
392      * <li>If two different jars contain classes in the same
393      * package, then it is not possible to load both jars into
394      * the same "lookaside" class loader (eg the system class loader)
395      * then map one of those subsets from here. Of course they could
396      * be loaded into two different "lookaside" class loaders and
397      * then a prefix used to map from here to one of those class loaders.
398      * </ul>
399      */
400     public void useExplicitLoader(final String prefix, final ClassLoader loader) {
401         if (lookasides == null) {
402             lookasides = new HashMap<>();
403         }
404         lookasides.put(prefix, loader);
405     }
406 
407     /**
408      * For classes with the specified prefix, get them from the system
409      * classpath <em>which is active at the point this method is called</em>.
410      * <p>
411      * This method is just a shortcut for
412      * <pre>
413      * useExplicitLoader(prefix, ClassLoader.getSystemClassLoader());
414      * </pre>
415      * <p>
416      * Of course, this assumes that the classes of interest are already
417      * in the classpath of the system class loader.
418      */
419     public void useSystemLoader(final String prefix) {
420         useExplicitLoader(prefix, ClassLoader.getSystemClassLoader());
421 
422     }
423 }