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