001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     * 
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     * 
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */ 
017    package org.apache.commons.logging.pathable;
018    
019    import java.net.URL;
020    import java.util.ArrayList;
021    import java.util.Arrays;
022    import java.util.Enumeration;
023    import java.util.HashSet;
024    import java.util.Set;
025    
026    import junit.framework.Test;
027    import junit.framework.TestCase;
028    
029    import org.apache.commons.logging.PathableClassLoader;
030    import org.apache.commons.logging.PathableTestSuite;
031    
032    /**
033     * Tests for the PathableTestSuite and PathableClassLoader functionality,
034     * where lookup order for the PathableClassLoader is child-first.
035     * <p>
036     * These tests assume:
037     * <ul>
038     * <li>junit is in system classpath
039     * <li>nothing else is in system classpath
040     * </ul>
041     */
042    
043    public class ChildFirstTestCase extends TestCase {
044        
045        /**
046         * Set up a custom classloader hierarchy for this test case.
047         * The hierarchy is:
048         * <ul>
049         * <li> contextloader: child-first.
050         * <li> childloader: child-first, used to load test case.
051         * <li> parentloader: child-first, parent is the bootclassloader.
052         * </ul>
053         */
054        public static Test suite() throws Exception {
055            Class thisClass = ChildFirstTestCase.class;
056            ClassLoader thisClassLoader = thisClass.getClassLoader();
057            
058            // Make the parent a direct child of the bootloader to hide all
059            // other classes in the system classpath
060            PathableClassLoader parent = new PathableClassLoader(null);
061            parent.setParentFirst(false);
062            
063            // Make the junit classes visible as a special case, as junit
064            // won't be able to call this class at all without this. The
065            // junit classes must be visible from the classloader that loaded
066            // this class, so use that as the source for future access to classes
067            // from the junit package.
068            parent.useExplicitLoader("junit.", thisClassLoader);
069            
070            // Make the commons-logging.jar classes visible via the parent
071            parent.addLogicalLib("commons-logging");
072            
073            // Create a child classloader to load the test case through
074            PathableClassLoader child = new PathableClassLoader(parent);
075            child.setParentFirst(false);
076            
077            // Obviously, the child classloader needs to have the test classes
078            // in its path!
079            child.addLogicalLib("testclasses");
080            child.addLogicalLib("commons-logging-adapters");
081            
082            // Create a third classloader to be the context classloader.
083            PathableClassLoader context = new PathableClassLoader(child);
084            context.setParentFirst(false);
085    
086            // reload this class via the child classloader
087            Class testClass = child.loadClass(thisClass.getName());
088            
089            // and return our custom TestSuite class
090            return new PathableTestSuite(testClass, context);
091        }
092    
093        /**
094         * Utility method to return the set of all classloaders in the
095         * parent chain starting from the one that loaded the class for
096         * this object instance.
097         */
098        private Set getAncestorCLs() {
099            Set s = new HashSet();
100            ClassLoader cl = this.getClass().getClassLoader();
101            while (cl != null) {
102                s.add(cl);
103                cl = cl.getParent();
104            }
105            return s;
106        }
107    
108        /**
109         * Test that the classloader hierarchy is as expected, and that
110         * calling loadClass() on various classloaders works as expected.
111         * Note that for this test case, parent-first classloading is
112         * in effect.
113         */
114        public void testPaths() throws Exception {
115            // the context classloader is not expected to be null
116            ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
117            assertNotNull("Context classloader is null", contextLoader);
118            assertEquals("Context classloader has unexpected type",
119                    PathableClassLoader.class.getName(),
120                    contextLoader.getClass().getName());
121            
122            // the classloader that loaded this class is obviously not null
123            ClassLoader thisLoader = this.getClass().getClassLoader();
124            assertNotNull("thisLoader is null", thisLoader);
125            assertEquals("thisLoader has unexpected type",
126                    PathableClassLoader.class.getName(),
127                    thisLoader.getClass().getName());
128            
129            // the suite method specified that the context classloader's parent
130            // is the loader that loaded this test case.
131            assertSame("Context classloader is not child of thisLoader",
132                    thisLoader, contextLoader.getParent());
133    
134            // thisLoader's parent should be available
135            ClassLoader parentLoader = thisLoader.getParent();
136            assertNotNull("Parent classloader is null", parentLoader);
137            assertEquals("Parent classloader has unexpected type",
138                    PathableClassLoader.class.getName(),
139                    parentLoader.getClass().getName());
140            
141            // parent should have a parent of null
142            assertNull("Parent classloader has non-null parent", parentLoader.getParent());
143    
144            // getSystemClassloader is not a PathableClassLoader; it's of a
145            // built-in type. This also verifies that system classloader is none of
146            // (context, child, parent).
147            ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
148            assertNotNull("System classloader is null", systemLoader);
149            assertFalse("System classloader has unexpected type",
150                    PathableClassLoader.class.getName().equals(
151                            systemLoader.getClass().getName()));
152    
153            // junit classes should be visible; their classloader is not
154            // in the hierarchy of parent classloaders for this class,
155            // though it is accessable due to trickery in the PathableClassLoader.
156            Class junitTest = contextLoader.loadClass("junit.framework.Test");
157            Set ancestorCLs = getAncestorCLs();
158            assertFalse("Junit not loaded by ancestor classloader", 
159                    ancestorCLs.contains(junitTest.getClassLoader()));
160    
161            // jcl api classes should be visible only via the parent
162            Class logClass = contextLoader.loadClass("org.apache.commons.logging.Log");
163            assertSame("Log class not loaded via parent",
164                    logClass.getClassLoader(), parentLoader);
165    
166            // jcl adapter classes should be visible via both parent and child. However
167            // as the classloaders are child-first we should see the child one.
168            Class log4jClass = contextLoader.loadClass("org.apache.commons.logging.impl.Log4JLogger");
169            assertSame("Log4JLogger not loaded via child", 
170                    log4jClass.getClassLoader(), thisLoader);
171            
172            // test classes should be visible via the child only
173            Class testClass = contextLoader.loadClass("org.apache.commons.logging.PathableTestSuite");
174            assertSame("PathableTestSuite not loaded via child", 
175                    testClass.getClassLoader(), thisLoader);
176            
177            // test loading of class that is not available
178            try {
179                Class noSuchClass = contextLoader.loadClass("no.such.class");
180                fail("Class no.such.class is unexpectedly available");
181                assertNotNull(noSuchClass); // silence warning about unused var
182            } catch(ClassNotFoundException ex) {
183                // ok
184            }
185    
186            // String class classloader is null
187            Class stringClass = contextLoader.loadClass("java.lang.String");
188            assertNull("String class classloader is not null!",
189                    stringClass.getClassLoader());
190        }
191        
192        /**
193         * Test that the various flavours of ClassLoader.getResource work as expected.
194         */
195        public void testResource() {
196            URL resource;
197            
198            ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
199            ClassLoader childLoader = contextLoader.getParent();
200            
201            // getResource where it doesn't exist
202            resource = childLoader.getResource("nosuchfile");
203            assertNull("Non-null URL returned for invalid resource name", resource);
204    
205            // getResource where it is accessable only to parent classloader
206            resource = childLoader.getResource("org/apache/commons/logging/Log.class");
207            assertNotNull("Unable to locate Log.class resource", resource);
208            
209            // getResource where it is accessable only to child classloader
210            resource = childLoader.getResource("org/apache/commons/logging/PathableTestSuite.class");
211            assertNotNull("Unable to locate PathableTestSuite.class resource", resource);
212    
213            // getResource where it is accessable to both classloaders. The one visible
214            // to the child should be returned. The URL returned will be of form
215            //  jar:file:/x/y.jar!path/to/resource. The filename part should include the jarname
216            // of form commons-logging-adapters-nnnn.jar, not commons-logging-nnnn.jar
217            resource = childLoader.getResource("org/apache/commons/logging/impl/Log4JLogger.class");
218            assertNotNull("Unable to locate Log4JLogger.class resource", resource);
219            assertTrue("Incorrect source for Log4JLogger class",
220                    resource.toString().indexOf("/commons-logging-adapters-1.") > 0);
221        }
222        
223        /**
224         * Test that the various flavours of ClassLoader.getResources work as expected.
225         */
226        public void testResources() throws Exception {
227            Enumeration resources;
228            URL[] urls;
229            
230            // verify the classloader hierarchy
231            ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
232            ClassLoader childLoader = contextLoader.getParent();
233            ClassLoader parentLoader = childLoader.getParent();
234            ClassLoader bootLoader = parentLoader.getParent();
235            assertNull("Unexpected classloader hierarchy", bootLoader);
236            
237            // getResources where no instances exist
238            resources = childLoader.getResources("nosuchfile");
239            urls = toURLArray(resources);
240            assertEquals("Non-null URL returned for invalid resource name", 0, urls.length);
241            
242            // getResources where the resource only exists in the parent
243            resources = childLoader.getResources("org/apache/commons/logging/Log.class");
244            urls = toURLArray(resources);
245            assertEquals("Unexpected number of Log.class resources found", 1, urls.length);
246            
247            // getResources where the resource only exists in the child
248            resources = childLoader.getResources("org/apache/commons/logging/PathableTestSuite.class");
249            urls = toURLArray(resources);
250            assertEquals("Unexpected number of PathableTestSuite.class resources found", 1, urls.length);
251            
252            // getResources where the resource exists in both.
253            // resources should be returned in order (child-resource, parent-resource).
254            //
255            // IMPORTANT: due to the fact that in java 1.4 and earlier method
256            // ClassLoader.getResources is final it isn't possible for PathableClassLoader
257            // to override this. So even when child-first is enabled the resource order
258            // is still (parent-resources, child-resources). This test verifies the expected
259            // behaviour - even though it's not the desired behaviour.
260            
261            resources = childLoader.getResources("org/apache/commons/logging/impl/Log4JLogger.class");
262            urls = toURLArray(resources);
263            assertEquals("Unexpected number of Log4JLogger.class resources found", 2, urls.length);
264            
265            // There is no gaurantee about the ordering of results returned from getResources
266            // To make this test portable across JVMs, sort the string to give them a known order
267            String[] urlsToStrings = new String[2];
268            urlsToStrings[0] = urls[0].toString();
269            urlsToStrings[1] = urls[1].toString();
270            Arrays.sort(urlsToStrings);
271            assertTrue("Incorrect source for Log4JLogger class",
272                    urlsToStrings[0].indexOf("/commons-logging-1.") > 0);
273            assertTrue("Incorrect source for Log4JLogger class",
274                    urlsToStrings[1].indexOf("/commons-logging-adapters-1.") > 0);
275        }
276    
277        /**
278         * Utility method to convert an enumeration-of-URLs into an array of URLs.
279         */
280        private static URL[] toURLArray(Enumeration e) {
281            ArrayList l = new ArrayList();
282            while (e.hasMoreElements()) {
283                URL u = (URL) e.nextElement();
284                l.add(u);
285            }
286            URL[] tmp = new URL[l.size()];
287            return (URL[]) l.toArray(tmp);
288        }
289    
290        /**
291         * Test that getResourceAsStream works.
292         */
293        public void testResourceAsStream() throws Exception {
294            java.io.InputStream is;
295            
296            // verify the classloader hierarchy
297            ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
298            ClassLoader childLoader = contextLoader.getParent();
299            ClassLoader parentLoader = childLoader.getParent();
300            ClassLoader bootLoader = parentLoader.getParent();
301            assertNull("Unexpected classloader hierarchy", bootLoader);
302            
303            // getResourceAsStream where no instances exist
304            is = childLoader.getResourceAsStream("nosuchfile");
305            assertNull("Invalid resource returned non-null stream", is);
306            
307            // getResourceAsStream where resource does exist
308            is = childLoader.getResourceAsStream("org/apache/commons/logging/Log.class");
309            assertNotNull("Null returned for valid resource", is);
310            is.close();
311            
312            // It would be nice to test parent-first ordering here, but that would require
313            // having a resource with the same name in both the parent and child loaders,
314            // but with different contents. That's a little tricky to set up so we'll
315            // skip that for now.
316        }
317    }