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 }