View Javadoc
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  package org.apache.commons.logging.pathable;
18  
19  import java.net.URL;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.Enumeration;
23  import java.util.HashSet;
24  import java.util.Set;
25  
26  import org.apache.commons.logging.Artifacts;
27  import org.apache.commons.logging.PathableClassLoader;
28  import org.apache.commons.logging.PathableTestSuite;
29  
30  import junit.framework.Test;
31  import junit.framework.TestCase;
32  
33  /**
34   * Tests for the PathableTestSuite and PathableClassLoader functionality,
35   * where lookup order for the PathableClassLoader is child-first.
36   * <p>
37   * These tests assume:
38   * <ul>
39   * <li>junit is in system classpath
40   * <li>nothing else is in system classpath
41   * </ul>
42   */
43  
44  public class ChildFirstTestCase extends TestCase {
45  
46      /**
47       * Sets up a custom class loader hierarchy for this test case.
48       * The hierarchy is:
49       * <ul>
50       * <li> contextloader: child-first.
51       * <li> childloader: child-first, used to load test case.
52       * <li> parentloader: child-first, parent is the bootclass loader.
53       * </ul>
54       */
55      public static Test suite() throws Exception {
56          final Class thisClass = ChildFirstTestCase.class;
57          final ClassLoader thisClassLoader = thisClass.getClassLoader();
58  
59          // Make the parent a direct child of the bootloader to hide all
60          // other classes in the system classpath
61          final PathableClassLoader parent = new PathableClassLoader(null);
62          parent.setParentFirst(false);
63  
64          // Make the junit classes visible as a special case, as junit
65          // won't be able to call this class at all without this. The
66          // junit classes must be visible from the class loader that loaded
67          // this class, so use that as the source for future access to classes
68          // from the junit package.
69          parent.useExplicitLoader("junit.", thisClassLoader);
70          parent.useExplicitLoader("org.junit.", thisClassLoader);
71  
72          // Make the commons-logging.jar classes visible via the parent
73          parent.addLogicalLib("commons-logging");
74  
75          // Create a child class loader to load the test case through
76          final PathableClassLoader child = new PathableClassLoader(parent);
77          child.setParentFirst(false);
78  
79          // Obviously, the child class loader needs to have the test classes
80          // in its path!
81          child.addLogicalLib("testclasses");
82          child.addLogicalLib("commons-logging-adapters");
83  
84          // Create a third class loader to be the context class loader.
85          final PathableClassLoader context = new PathableClassLoader(child);
86          context.setParentFirst(false);
87  
88          // reload this class via the child class loader
89          final Class testClass = child.loadClass(thisClass.getName());
90  
91          // and return our custom TestSuite class
92          return new PathableTestSuite(testClass, context);
93      }
94  
95      /**
96       * Utility method to convert an enumeration-of-URLs into an array of URLs.
97       */
98      private static URL[] toURLArray(final Enumeration e) {
99          final ArrayList l = new ArrayList();
100         while (e.hasMoreElements()) {
101             final URL u = (URL) e.nextElement();
102             l.add(u);
103         }
104         final URL[] tmp = new URL[l.size()];
105         return (URL[]) l.toArray(tmp);
106     }
107 
108     /**
109      * Utility method to return the set of all class loaders in the
110      * parent chain starting from the one that loaded the class for
111      * this object instance.
112      */
113     private Set getAncestorCLs() {
114         final Set s = new HashSet();
115         ClassLoader cl = this.getClass().getClassLoader();
116         while (cl != null) {
117             s.add(cl);
118             cl = cl.getParent();
119         }
120         return s;
121     }
122 
123     /**
124      * Test that the class loader hierarchy is as expected, and that
125      * calling loadClass() on various class loaders works as expected.
126      * Note that for this test case, parent-first classloading is
127      * in effect.
128      */
129     public void testPaths() throws Exception {
130         // the context class loader is not expected to be null
131         final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
132         assertNotNull("Context class loader is null", contextLoader);
133         assertEquals("Context class loader has unexpected type",
134                 PathableClassLoader.class.getName(),
135                 contextLoader.getClass().getName());
136 
137         // the class loader that loaded this class is obviously not null
138         final ClassLoader thisLoader = this.getClass().getClassLoader();
139         assertNotNull("thisLoader is null", thisLoader);
140         assertEquals("thisLoader has unexpected type",
141                 PathableClassLoader.class.getName(),
142                 thisLoader.getClass().getName());
143 
144         // the suite method specified that the context class loader's parent
145         // is the loader that loaded this test case.
146         assertSame("Context class loader is not child of thisLoader",
147                 thisLoader, contextLoader.getParent());
148 
149         // thisLoader's parent should be available
150         final ClassLoader parentLoader = thisLoader.getParent();
151         assertNotNull("Parent class loader is null", parentLoader);
152         assertEquals("Parent class loader has unexpected type",
153                 PathableClassLoader.class.getName(),
154                 parentLoader.getClass().getName());
155 
156         // parent should have a parent of null
157         assertNull("Parent class loader has non-null parent", parentLoader.getParent());
158 
159         // getSystemClassloader is not a PathableClassLoader; it's of a
160         // built-in type. This also verifies that system class loader is none of
161         // (context, child, parent).
162         final ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
163         assertNotNull("System class loader is null", systemLoader);
164         assertFalse("System class loader has unexpected type",
165                 PathableClassLoader.class.getName().equals(
166                         systemLoader.getClass().getName()));
167 
168         // junit classes should be visible; their class loader is not
169         // in the hierarchy of parent class loaders for this class,
170         // though it is accessible due to trickery in the PathableClassLoader.
171         final Class junitTest = contextLoader.loadClass("junit.framework.Test");
172         final Set ancestorCLs = getAncestorCLs();
173         assertFalse("Junit not loaded by ancestor class loader",
174                 ancestorCLs.contains(junitTest.getClassLoader()));
175 
176         // jcl api classes should be visible only via the parent
177         final Class logClass = contextLoader.loadClass("org.apache.commons.logging.Log");
178         assertSame("Log class not loaded via parent",
179                 logClass.getClassLoader(), parentLoader);
180 
181         // jcl adapter classes should be visible via both parent and child. However
182         // as the class loaders are child-first we should see the child one.
183         final Class log4jClass = contextLoader.loadClass("org.apache.commons.logging.impl.Log4JLogger");
184         assertSame("Log4JLogger not loaded via child",
185                 log4jClass.getClassLoader(), thisLoader);
186 
187         // test classes should be visible via the child only
188         final Class testClass = contextLoader.loadClass("org.apache.commons.logging.PathableTestSuite");
189         assertSame("PathableTestSuite not loaded via child",
190                 testClass.getClassLoader(), thisLoader);
191 
192         // test loading of class that is not available
193         try {
194             final Class noSuchClass = contextLoader.loadClass("no.such.class");
195             fail("Class no.such.class is unexpectedly available");
196             assertNotNull(noSuchClass); // silence warning about unused var
197         } catch (final ClassNotFoundException ex) {
198             // ok
199         }
200 
201         // String class class loader is null
202         final Class stringClass = contextLoader.loadClass("java.lang.String");
203         assertNull("String class class loader is not null!",
204                 stringClass.getClassLoader());
205     }
206 
207     /**
208      * Test that the various flavors of ClassLoader.getResource work as expected.
209      */
210     public void testResource() {
211         URL resource;
212 
213         final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
214         final ClassLoader childLoader = contextLoader.getParent();
215 
216         // getResource where it doesn't exist
217         resource = childLoader.getResource("nosuchfile");
218         assertNull("Non-null URL returned for invalid resource name", resource);
219 
220         // getResource where it is accessible only to parent class loader
221         resource = childLoader.getResource("org/apache/commons/logging/Log.class");
222         assertNotNull("Unable to locate Log.class resource", resource);
223 
224         // getResource where it is accessible only to child class loader
225         resource = childLoader.getResource("org/apache/commons/logging/PathableTestSuite.class");
226         assertNotNull("Unable to locate PathableTestSuite.class resource", resource);
227 
228         // getResource where it is accessible to both class loaders. The one visible
229         // to the child should be returned. The URL returned will be of form
230         //  jar:file:/x/y.jar!path/to/resource. The file name part should include the jarname
231         // of form commons-logging-adapters-nnnn.jar, not commons-logging-nnnn.jar
232         resource = childLoader.getResource("org/apache/commons/logging/impl/Log4JLogger.class");
233         assertNotNull("Unable to locate Log4JLogger.class resource", resource);
234         assertTrue("Incorrect source for Log4JLogger class",
235                 resource.toString().indexOf(Artifacts.getAdaptersJarName()) > 0);
236     }
237 
238     /**
239      * Test that getResourceAsStream works.
240      */
241     public void testResourceAsStream() throws Exception {
242         java.io.InputStream is;
243 
244         // verify the class loader hierarchy
245         final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
246         final ClassLoader childLoader = contextLoader.getParent();
247         final ClassLoader parentLoader = childLoader.getParent();
248         final ClassLoader bootLoader = parentLoader.getParent();
249         assertNull("Unexpected class loader hierarchy", bootLoader);
250 
251         // getResourceAsStream where no instances exist
252         is = childLoader.getResourceAsStream("nosuchfile");
253         assertNull("Invalid resource returned non-null stream", is);
254 
255         // getResourceAsStream where resource does exist
256         is = childLoader.getResourceAsStream("org/apache/commons/logging/Log.class");
257         assertNotNull("Null returned for valid resource", is);
258         is.close();
259 
260         // It would be nice to test parent-first ordering here, but that would require
261         // having a resource with the same name in both the parent and child loaders,
262         // but with different contents. That's a little tricky to set up so we'll
263         // skip that for now.
264     }
265 
266     /**
267      * Test that the various flavors of ClassLoader.getResources work as expected.
268      */
269     public void testResources() throws Exception {
270         Enumeration resources;
271         URL[] urls;
272 
273         // verify the class loader hierarchy
274         final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
275         final ClassLoader childLoader = contextLoader.getParent();
276         final ClassLoader parentLoader = childLoader.getParent();
277         final ClassLoader bootLoader = parentLoader.getParent();
278         assertNull("Unexpected class loader hierarchy", bootLoader);
279 
280         // getResources where no instances exist
281         resources = childLoader.getResources("nosuchfile");
282         urls = toURLArray(resources);
283         assertEquals("Non-null URL returned for invalid resource name", 0, urls.length);
284 
285         // getResources where the resource only exists in the parent
286         resources = childLoader.getResources("org/apache/commons/logging/Log.class");
287         urls = toURLArray(resources);
288         assertEquals("Unexpected number of Log.class resources found", 1, urls.length);
289 
290         // getResources where the resource only exists in the child
291         resources = childLoader.getResources("org/apache/commons/logging/PathableTestSuite.class");
292         urls = toURLArray(resources);
293         assertEquals("Unexpected number of PathableTestSuite.class resources found", 1, urls.length);
294 
295         // getResources where the resource exists in both.
296         // resources should be returned in order (child-resource, parent-resource).
297         //
298         // IMPORTANT: due to the fact that in java 1.4 and earlier method
299         // ClassLoader.getResources is final it isn't possible for PathableClassLoader
300         // to override this. So even when child-first is enabled the resource order
301         // is still (parent-resources, child-resources). This test verifies the expected
302         // behavior - even though it's not the desired behavior.
303 
304         resources = childLoader.getResources("org/apache/commons/logging/impl/Log4JLogger.class");
305         urls = toURLArray(resources);
306         assertEquals("Unexpected number of Log4JLogger.class resources found", 2, urls.length);
307 
308         // There is no guarantee about the ordering of results returned from getResources
309         // To make this test portable across JVMs, sort the string to give them a known order
310         final String[] urlsToStrings = new String[2];
311         urlsToStrings[0] = urls[0].toString();
312         urlsToStrings[1] = urls[1].toString();
313         Arrays.sort(urlsToStrings);
314         assertTrue("Incorrect source for Log4JLogger class",
315                 urlsToStrings[0].indexOf(Artifacts.getAdaptersJarName()) > 0);
316         assertTrue("Incorrect source for Log4JLogger class",
317                 urlsToStrings[1].indexOf(Artifacts.getMainJarName()) > 0);
318     }
319 }