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 parent-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 ParentFirstTestCase extends TestCase {
044        
045        /**
046         * Set up a custom classloader hierarchy for this test case.
047         * The hierarchy is:
048         * <ul>
049         * <li> contextloader: parent-first.
050         * <li> childloader: parent-first, used to load test case.
051         * <li> parentloader: parent-first, parent is the bootclassloader.
052         * </ul>
053         */
054        public static Test suite() throws Exception {
055            Class thisClass = ParentFirstTestCase.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            
062            // Make the junit classes visible as a special case, as junit
063            // won't be able to call this class at all without this. The
064            // junit classes must be visible from the classloader that loaded
065            // this class, so use that as the source for future access to classes
066            // from the junit package.
067            parent.useExplicitLoader("junit.", thisClassLoader);
068            
069            // make the commons-logging.jar classes visible via the parent
070            parent.addLogicalLib("commons-logging");
071            
072            // create a child classloader to load the test case through
073            PathableClassLoader child = new PathableClassLoader(parent);
074            
075            // obviously, the child classloader needs to have the test classes
076            // in its path!
077            child.addLogicalLib("testclasses");
078            child.addLogicalLib("commons-logging-adapters");
079            
080            // create a third classloader to be the context classloader.
081            PathableClassLoader context = new PathableClassLoader(child);
082    
083            // reload this class via the child classloader
084            Class testClass = child.loadClass(thisClass.getName());
085            
086            // and return our custom TestSuite class
087            return new PathableTestSuite(testClass, context);
088        }
089        
090        /**
091         * Utility method to return the set of all classloaders in the
092         * parent chain starting from the one that loaded the class for
093         * this object instance.
094         */
095        private Set getAncestorCLs() {
096            Set s = new HashSet();
097            ClassLoader cl = this.getClass().getClassLoader();
098            while (cl != null) {
099                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    }