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