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 }