PathableClassLoader.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.logging;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import junit.framework.Assert;
/**
* A ClassLoader which sees only specified classes, and which can be
* set to do parent-first or child-first path lookup.
* <p>
* Note that this class loader is not "industrial strength"; users
* looking for such a class may wish to look at the Tomcat sourcecode
* instead. In particular, this class may not be threadsafe.
* <p>
* Note that the ClassLoader.getResources method isn't overloaded here.
* It would be nice to ensure that when child-first lookup is set the
* resources from the child are returned earlier in the list than the
* resources from the parent. However overriding this method isn't possible
* as the Java 1.4 version of ClassLoader declares this method final
* (though the Java 1.5 version has removed the final qualifier). As the
* ClassLoader Javadoc doesn't specify the order in which resources
* are returned, it's valid to return the resources in any order (just
* untidy) so the inherited implementation is technically ok.
*/
public class PathableClassLoader extends URLClassLoader {
private static final URL[] NO_URLS = {};
/**
* A map of package-prefix to ClassLoader. Any class which is in
* this map is looked up via the specified class loader instead of
* the classpath associated with this class loader or its parents.
* <p>
* This is necessary in order for the rest of the world to communicate
* with classes loaded via a custom class loader. As an example, junit
* tests which are loaded via a custom class loader needs to see
* the same junit classes as the code invoking the test, otherwise
* they can't pass result objects back.
* <p>
* Normally, only a class loader created with a null parent needs to
* have any lookasides defined.
*/
private HashMap<String, ClassLoader> lookasides;
/**
* See setParentFirst.
*/
private boolean parentFirst = true;
/**
* Constructs a new instance.
* <p>
* Often, null is passed as the parent, that is, the parent of the new
* instance is the bootloader. This ensures that the classpath is
* totally clean; nothing but the standard Java library will be
* present.
* <p>
* When using a null parent class loader with a junit test, it *is*
* necessary for the junit library to also be visible. In this case, it
* is recommended that the following code be used:
* <pre>
* pathableLoader.useExplicitLoader(
* "junit.",
* junit.framework.Test.class.getClassLoader());
* </pre>
* Note that this works regardless of whether junit is on the system
* classpath, or whether it has been loaded by some test framework that
* creates its own class loader to run unit tests in (eg maven2's
* Surefire plugin).
*/
public PathableClassLoader(final ClassLoader parent) {
super(NO_URLS, parent);
}
/**
* Specify a logical library to be included in the classpath used to
* locate classes.
* <p>
* The specified lib name is used as a key into the system properties;
* there is expected to be a system property defined with that name
* whose value is a url that indicates where that logical library can
* be found. Typically this is the name of a jar file, or a directory
* containing class files.
* <p>
* If there is no system property, but the class loader that loaded
* this class is a URLClassLoader then the set of URLs that the
* class loader uses for its classpath is scanned; any jar in the
* URL set whose name starts with the specified string is added to
* the classpath managed by this instance.
* <p>
* Using logical library names allows the calling code to specify its
* desired classpath without knowing the exact location of the necessary
* classes.
*/
public void addLogicalLib(final String logicalLib) {
// first, check the system properties
final String fileName = System.getProperty(logicalLib);
if (fileName != null) {
try {
final File file = new File(fileName);
if (!file.exists()) {
Assert.fail("Unable to add logical library " + fileName);
}
addURL(file.toURI().toURL());
return;
} catch (final MalformedURLException e) {
throw new UnknownError(
"Invalid file [" + fileName + "] for logical lib [" + logicalLib + "]");
}
}
// now check the classpath for a similar-named lib
final URL libUrl = libFromClasspath(logicalLib);
if (libUrl != null) {
addURL(libUrl);
return;
}
// lib not found
throw new UnknownError(
"Logical lib [" + logicalLib + "] is not defined"
+ " as a System property.");
}
/**
* Specify a collection of logical libraries. See addLogicalLib.
*/
public void addLogicalLib(final String[] logicalLibs) {
for (final String logicalLib : logicalLibs) {
addLogicalLib(logicalLib);
}
}
/**
* Allow caller to explicitly add paths. Generally this not a good idea;
* use addLogicalLib instead, then define the location for that logical
* library in the build.xml file.
*/
@Override
public void addURL(final URL url) {
super.addURL(url);
}
/**
* Same as parent class method except that when parentFirst is false
* the resource is looked for in the local classpath before the parent
* loader is consulted.
*/
@Override
public URL getResource(final String name) {
if (parentFirst) {
return super.getResource(name);
}
final URL local = super.findResource(name);
if (local != null) {
return local;
}
return super.getResource(name);
}
/**
* Same as parent class method except that when parentFirst is false
* the resource is looked for in the local classpath before the parent
* loader is consulted.
*/
@Override
public InputStream getResourceAsStream(final String name) {
if (parentFirst) {
return super.getResourceAsStream(name);
}
final URL local = super.findResource(name);
if (local != null) {
try {
return local.openStream();
} catch (final IOException e) {
// TODO: check if this is right or whether we should
// fall back to trying parent. The Javadoc doesn't say...
return null;
}
}
return super.getResourceAsStream(name);
}
/**
* Emulate a proper implementation of getResources which respects the
* setting for parentFirst.
* <p>
* Note that it's not possible to override the inherited getResources, as
* it's declared final in java1.4 (thought that's been removed for 1.5).
* The inherited implementation always behaves as if parentFirst=true.
*/
public Enumeration<URL> getResourcesInOrder(final String name) throws IOException {
if (parentFirst) {
return super.getResources(name);
}
final Enumeration<URL> localUrls = super.findResources(name);
final ClassLoader parent = getParent();
if (parent == null) {
// Alas, there is no method to get matching resources
// from a null (BOOT) parent class loader. Calling
// ClassLoader.getSystemClassLoader isn't right. Maybe
// calling Class.class.getResources(name) would do?
//
// However for the purposes of unit tests, we can
// simply assume that no relevant resources are
// loadable from the parent; unit tests will never be
// putting any of their resources in a "boot" class loader
// path!
return localUrls;
}
final Enumeration<URL> parentUrls = parent.getResources(name);
final ArrayList<URL> localItems = toList(localUrls);
final ArrayList<URL> parentItems = toList(parentUrls);
localItems.addAll(parentItems);
return Collections.enumeration(localItems);
}
/**
* If the class loader that loaded this class has this logical lib in its
* path, then return the matching URL otherwise return null.
* <p>
* This only works when the class loader loading this class is an instance
* of URLClassLoader and thus has a getURLs method that returns the classpath
* it uses when loading classes. However in practice, the vast majority of the
* time this type is the class loader used.
* <p>
* The classpath of the class loader for this instance is scanned, and any
* jarfile in the path whose name starts with the logicalLib string is
* considered a match. For example, passing "foo" will match a url
* of {@code file:///some/where/foo-2.7.jar}.
* <p>
* When multiple classpath entries match the specified logicalLib string,
* the one with the shortest file name component is returned. This means that
* if "foo-1.1.jar" and "foobar-1.1.jar" are in the path, then a logicalLib
* name of "foo" will match the first entry above.
*/
private URL libFromClasspath(final String logicalLib) {
final ClassLoader cl = this.getClass().getClassLoader();
if (!(cl instanceof URLClassLoader)) {
return null;
}
final URLClassLoader ucl = (URLClassLoader) cl;
final URL[] path = ucl.getURLs();
URL shortestMatch = null;
int shortestMatchLen = Integer.MAX_VALUE;
for (final URL u : path) {
// extract the file name bit on the end of the URL
String fileName = u.toString();
if (!fileName.endsWith(".jar")) {
// not a jarfile, ignore it
continue;
}
final int lastSlash = fileName.lastIndexOf('/');
if (lastSlash >= 0) {
fileName = fileName.substring(lastSlash+1);
}
// ok, this is a candidate
if (fileName.startsWith(logicalLib) && fileName.length() < shortestMatchLen) {
shortestMatch = u;
shortestMatchLen = fileName.length();
}
}
return shortestMatch;
}
/**
* Override ClassLoader method.
* <p>
* For each explicitly mapped package prefix, if the name matches the
* prefix associated with that entry then attempt to load the class via
* that entries' class loader.
*/
@Override
protected Class<?> loadClass(final String name, final boolean resolve)
throws ClassNotFoundException {
// just for performance, check java and javax
if (name.startsWith("java.") || name.startsWith("javax.")) {
return super.loadClass(name, resolve);
}
if (lookasides != null) {
for (final Map.Entry<String, ClassLoader> entry : lookasides.entrySet()) {
final String prefix = entry.getKey();
if (name.startsWith(prefix)) {
final ClassLoader loader = entry.getValue();
return Class.forName(name, resolve, loader);
}
}
}
if (parentFirst) {
return super.loadClass(name, resolve);
}
try {
Class<?> clazz = findLoadedClass(name);
if (clazz == null) {
clazz = super.findClass(name);
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
} catch (final ClassNotFoundException e) {
return super.loadClass(name, resolve);
}
}
/**
* Specify whether this class loader should ask the parent class loader
* to resolve a class first, before trying to resolve it via its own
* classpath.
* <p>
* Checking with the parent first is the normal approach for java, but
* components within containers such as servlet engines can use
* child-first lookup instead, to allow the components to override libs
* which are visible in shared class loaders provided by the container.
* <p>
* Note that the method getResources always behaves as if parentFirst=true,
* because of limitations in Java 1.4; see the Javadoc for method
* getResourcesInOrder for details.
* <p>
* This value defaults to true.
*/
public void setParentFirst(final boolean state) {
parentFirst = state;
}
/**
* Clean implementation of list function of
* {@link java.util.Collection} added in JDK 1.4
* @param en {@code Enumeration}, possibly null
* @return {@code ArrayList} containing the enumerated
* elements in the enumerated order, not null
*/
private <E> ArrayList<E> toList(final Enumeration<E> en) {
final ArrayList<E> results = new ArrayList<>();
if (en != null) {
while (en.hasMoreElements()) {
results.add(en.nextElement());
}
}
return results;
}
/**
* Specify a class loader to use for specific Java packages.
* <p>
* The specified class loader is normally a loader that is NOT
* an ancestor of this class loader. In particular, this loader
* may have the bootloader as its parent, but be configured to
* see specific other classes (eg the junit library loaded
* via the system class loader).
* <p>
* The differences between using this method, and using
* addLogicalLib are:
* <ul>
* <li>If code calls getClassLoader on a class loaded via
* "lookaside", then traces up its inheritance chain, it
* will see the "real" class loaders. When the class is remapped
* into this class loader via addLogicalLib, the class loader
* chain seen is this object plus ancestors.
* <li>If two different jars contain classes in the same
* package, then it is not possible to load both jars into
* the same "lookaside" class loader (eg the system class loader)
* then map one of those subsets from here. Of course they could
* be loaded into two different "lookaside" class loaders and
* then a prefix used to map from here to one of those class loaders.
* </ul>
*/
public void useExplicitLoader(final String prefix, final ClassLoader loader) {
if (lookasides == null) {
lookasides = new HashMap<>();
}
lookasides.put(prefix, loader);
}
/**
* For classes with the specified prefix, get them from the system
* classpath <em>which is active at the point this method is called</em>.
* <p>
* This method is just a shortcut for
* <pre>
* useExplicitLoader(prefix, ClassLoader.getSystemClassLoader());
* </pre>
* <p>
* Of course, this assumes that the classes of interest are already
* in the classpath of the system class loader.
*/
public void useSystemLoader(final String prefix) {
useExplicitLoader(prefix, ClassLoader.getSystemClassLoader());
}
}