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