001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     * 
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     * 
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */ 
017    
018    package org.apache.commons.logging;
019    
020    import java.io.File;
021    import java.io.IOException;
022    import java.io.InputStream;
023    import java.net.URL;
024    import java.net.URLClassLoader;
025    import java.util.ArrayList;
026    import java.util.Collections;
027    import java.util.Enumeration;
028    import java.util.HashMap;
029    import java.util.Iterator;
030    import java.util.Map;
031    
032    /**
033     * A ClassLoader which sees only specified classes, and which can be
034     * set to do parent-first or child-first path lookup.
035     * <p>
036     * Note that this classloader is not "industrial strength"; users
037     * looking for such a class may wish to look at the Tomcat sourcecode
038     * instead. In particular, this class may not be threadsafe.
039     * <p>
040     * Note that the ClassLoader.getResources method isn't overloaded here.
041     * It would be nice to ensure that when child-first lookup is set the
042     * resources from the child are returned earlier in the list than the
043     * resources from the parent. However overriding this method isn't possible
044     * as the java 1.4 version of ClassLoader declares this method final
045     * (though the java 1.5 version has removed the final qualifier). As the
046     * ClassLoader javadoc doesn't specify the order in which resources
047     * are returned, it's valid to return the resources in any order (just
048     * untidy) so the inherited implementation is technically ok.
049     */
050    
051    public class PathableClassLoader extends URLClassLoader {
052        
053        private static final URL[] NO_URLS = new URL[0];
054        
055        /**
056         * A map of package-prefix to ClassLoader. Any class which is in
057         * this map is looked up via the specified classloader instead of
058         * the classpath associated with this classloader or its parents.
059         * <p>
060         * This is necessary in order for the rest of the world to communicate
061         * with classes loaded via a custom classloader. As an example, junit
062         * testcases which are loaded via a custom classloader needs to see
063         * the same junit classes as the code invoking the testcase, otherwise
064         * they can't pass result objects back. 
065         * <p>
066         * Normally, only a classloader created with a null parent needs to
067         * have any lookasides defined.
068         */
069        private HashMap lookasides = null;
070    
071        /**
072         * See setParentFirst.
073         */
074        private boolean parentFirst = true;
075    
076        /**
077         * Constructor.
078         * <p>
079         * Often, null is passed as the parent, ie the parent of the new
080         * instance is the bootloader. This ensures that the classpath is
081         * totally clean; nothing but the standard java library will be
082         * present.
083         * <p>
084         * When using a null parent classloader with a junit testcase, it *is*
085         * necessary for the junit library to also be visible. In this case, it
086         * is recommended that the following code be used:
087         * <pre>
088         * pathableLoader.useExplicitLoader(
089         *   "junit.",
090         *   junit.framework.Test.class.getClassLoader());
091         * </pre>
092         * Note that this works regardless of whether junit is on the system
093         * classpath, or whether it has been loaded by some test framework that
094         * creates its own classloader to run unit tests in (eg maven2's
095         * Surefire plugin).
096         */
097        public PathableClassLoader(ClassLoader parent) {
098            super(NO_URLS, parent);
099        }
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    }