    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 }