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