1
0
Files
tweed5-commons-logging/src/test/org/apache/commons/logging/PathableClassLoader.java
Simon Kitching e7ab5b24d8 Allow libs for test to be discovered via the classpath as well as via system properties.
This has been implemented to suppprt running tests via the maven surefire plugin, but is
a general-purpose mechanism.


git-svn-id: https://svn.apache.org/repos/asf/jakarta/commons/proper/logging/trunk@427394 13f79535-47bb-0310-9956-ffa450edef68
2006-08-01 01:13:11 +00:00

407 lines
15 KiB
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
*
* http://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.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* 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 classloader 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 = new URL[0];
/**
* A map of package-prefix to ClassLoader. Any class which is in
* this map is looked up via the specified classloader instead of
* the classpath associated with this classloader or its parents.
* <p>
* This is necessary in order for the rest of the world to communicate
* with classes loaded via a custom classloader. As an example, junit
* testcases which are loaded via a custom classloader needs to see
* the same junit classes as the code invoking the testcase, otherwise
* they can't pass result objects back.
* <p>
* Normally, only a classloader created with a null parent needs to
* have any lookasides defined.
*/
private HashMap lookasides = null;
/**
* See setParentFirst.
*/
private boolean parentFirst = true;
/**
* Constructor.
* <p>
* Often, null is passed as the parent, ie 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 classloader with a junit testcase, 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 classloader to run unit tests in (eg maven2's
* Surefire plugin).
*/
public PathableClassLoader(ClassLoader parent) {
super(NO_URLS, parent);
}
/**
* 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.
*/
public void addURL(URL url) {
super.addURL(url);
}
/**
* Specify whether this classloader should ask the parent classloader
* 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 classloaders 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(boolean state) {
parentFirst = state;
}
/**
* For classes with the specified prefix, get them from the system
* classpath <i>which is active at the point this method is called</i>.
* <p>
* This method is just a shortcut for
* <pre>
* useExplicitLoader(prefix, ClassLoader.getSystemClassLoader());
* </pre>
*/
public void useSystemLoader(String prefix) {
useExplicitLoader(prefix, ClassLoader.getSystemClassLoader());
}
/**
* Specify a classloader to use for specific java packages.
* <p>
* The specified classloader is normally a loader that is NOT
* an ancestor of this classloader. 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 classloader).
* <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" classloaders. When the class is remapped
* into this classloader via addLogicalLib, the classloader
* 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" classloader (eg the system classloader)
* then map one of those subsets from here. Of course they could
* be loaded into two different "lookaside" classloaders and
* then a prefix used to map from here to one of those classloaders.
* </ul>
*/
public void useExplicitLoader(String prefix, ClassLoader loader) {
if (lookasides == null) {
lookasides = new HashMap();
}
lookasides.put(prefix, loader);
}
/**
* Specify a collection of logical libraries. See addLogicalLib.
*/
public void addLogicalLib(String[] logicalLibs) {
for(int i=0; i<logicalLibs.length; ++i) {
addLogicalLib(logicalLibs[i]);
}
}
/**
* 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>
* 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(String logicalLib) {
// first, check the system properties
String filename = System.getProperty(logicalLib);
if (filename != null) {
try {
URL libUrl = new File(filename).toURL();
addURL(libUrl);
return;
} catch(java.net.MalformedURLException e) {
throw new UnknownError(
"Invalid file [" + filename + "] for logical lib [" + logicalLib + "]");
}
}
// now check the classpath for a similar-named lib
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.");
}
private URL libFromClasspath(String logicalLib) {
ClassLoader cl = this.getClass().getClassLoader();
if (cl instanceof URLClassLoader == false) {
return null;
}
URLClassLoader ucl = (URLClassLoader) cl;
URL[] path = ucl.getURLs();
for(int i=0; i<path.length; ++i) {
URL u = path[i];
// extract the filename bit on the end of the url
String filename = u.toString();
if (!filename.endsWith(".jar")) {
// not a jarfile, ignore it
continue;
}
int lastSlash = filename.lastIndexOf('/');
if (lastSlash >= 0) {
filename = filename.substring(lastSlash+1);
}
if (filename.startsWith(logicalLib)) {
System.out.println("found lib " + logicalLib + " at url " + u);
return u;
} else {
System.out.println("lib " + logicalLib + " does not match [" + filename + "] at url " + u);
}
}
return null;
}
/**
* 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' classloader.
*/
protected Class loadClass(String name, 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(Iterator i = lookasides.entrySet().iterator(); i.hasNext(); ) {
Map.Entry entry = (Map.Entry) i.next();
String prefix = (String) entry.getKey();
if (name.startsWith(prefix) == true) {
ClassLoader loader = (ClassLoader) entry.getValue();
Class clazz = Class.forName(name, resolve, loader);
return clazz;
}
}
}
if (parentFirst) {
return super.loadClass(name, resolve);
} else {
// Implement child-first.
//
// It appears that the findClass method doesn't check whether the
// class has already been loaded. This seems odd to me, but without
// first checking via findLoadedClass we can get java.lang.LinkageError
// with message "duplicate class definition" which isn't good.
try {
Class clazz = findLoadedClass(name);
if (clazz == null) {
clazz = super.findClass(name);
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
} catch(ClassNotFoundException e) {
return super.loadClass(name, resolve);
}
}
}
/**
* 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.
*/
public URL getResource(String name) {
if (parentFirst) {
return super.getResource(name);
} else {
URL local = super.findResource(name);
if (local != null) {
return local;
}
return super.getResource(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 getResourcesInOrder(String name) throws IOException {
if (parentFirst) {
return super.getResources(name);
} else {
Enumeration localUrls = super.findResources(name);
ClassLoader parent = getParent();
if (parent == null) {
// Alas, there is no method to get matching resources
// from a null (BOOT) parent classloader. 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" classloader
// path!
return localUrls;
}
Enumeration parentUrls = parent.getResources(name);
ArrayList localItems = toList(localUrls);
ArrayList parentItems = toList(parentUrls);
localItems.addAll(parentItems);
return Collections.enumeration(localItems);
}
}
/**
*
* Clean implementation of list function of
* {@link java.utils.Collection} added in JDK 1.4
* @param en <code>Enumeration</code>, possibly null
* @return <code>ArrayList</code> containing the enumerated
* elements in the enumerated order, not null
*/
private ArrayList toList(Enumeration en) {
ArrayList results = new ArrayList();
if (en != null) {
while (en.hasMoreElements()){
Object element = en.nextElement();
results.add(element);
}
}
return results;
}
/**
* 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.
*/
public InputStream getResourceAsStream(String name) {
if (parentFirst) {
return super.getResourceAsStream(name);
} else {
URL local = super.findResource(name);
if (local != null) {
try {
return local.openStream();
} catch(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);
}
}
}