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 }