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 }