DefaultClassRealmTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.codehaus.plexus.classworlds.realm;

/*
 * Copyright 2001-2006 Codehaus Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;

import org.codehaus.plexus.classworlds.AbstractClassWorldsTestCase;
import org.codehaus.plexus.classworlds.ClassWorld;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

class DefaultClassRealmTest extends AbstractClassWorldsTestCase {
    // ----------------------------------------------------------------------
    // Class testing
    // ----------------------------------------------------------------------

    @Test
    void testLoadClassFromRealm() throws Exception {
        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);

        mainRealm.addURL(getJarUrl("component0-1.0.jar"));

        loadClass(mainRealm, "org.codehaus.plexus.Component0");
    }

    @Test
    void testLoadClassFromChildRealmWhereClassIsLocatedInParentRealm() throws Exception {
        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);

        mainRealm.addURL(getJarUrl("component0-1.0.jar"));

        ClassRealm childRealm = mainRealm.createChildRealm("child");

        loadClass(childRealm, "org.codehaus.plexus.Component0");
    }

    @Test
    void testLoadClassFromChildRealmWhereClassIsLocatedInGrantParentRealm() throws Exception {
        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);

        mainRealm.addURL(getJarUrl("component0-1.0.jar"));

        ClassRealm childRealm = mainRealm.createChildRealm("child");

        ClassRealm grandchildRealm = childRealm.createChildRealm("grandchild");

        loadClass(grandchildRealm, "org.codehaus.plexus.Component0");
    }

    @Test
    void testLoadClassFromChildRealmWhereClassIsLocatedInBothChildRealmAndParentRealm() throws Exception {
        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "parent", null);

        mainRealm.addURL(getJarUrl("component5-1.0.jar"));

        ClassRealm childRealm = mainRealm.createChildRealm("child");

        childRealm.addURL(getJarUrl("component5-2.0.jar"));

        Class<?> cls = loadClass(childRealm, "test.Component5");

        assertSame(childRealm, cls.getClassLoader());
        assertEquals(1, cls.getMethods().length);
        assertEquals("printNew", cls.getMethods()[0].getName());
    }

    @Test
    void testLoadNonExistentClass() {
        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);

        mainRealm.addURL(getJarUrl("component0-1.0.jar"));

        try {
            mainRealm.loadClass("org.foo.bar.NonExistentClass");

            fail("A ClassNotFoundException should have been thrown!");
        } catch (ClassNotFoundException e) {
            // expected
        }
    }

    @Test
    void testImport() throws Exception {
        ClassWorld world = new ClassWorld();

        ClassRealm r0 = world.newRealm("r0");

        ClassRealm r1 = world.newRealm("r1");

        r0.addURL(getJarUrl("component0-1.0.jar"));

        r1.importFrom("r0", "org.codehaus.plexus");

        loadClass(r1, "org.codehaus.plexus.Component0");
    }

    @Test
    void testParentImport() throws Exception {
        ClassWorld world = new ClassWorld();

        ClassRealm parent = world.newRealm("parent");

        ClassRealm child = world.newRealm("child");

        parent.addURL(getJarUrl("component0-1.0.jar"));

        child.setParentRealm(parent);

        Class<?> type = loadClass(child, "org.codehaus.plexus.Component0");

        child.importFromParent("non-existing");

        assertSame(null, loadClassOrNull(child, "org.codehaus.plexus.Component0"));

        child.importFromParent("org.codehaus.plexus");

        assertSame(type, loadClass(child, "org.codehaus.plexus.Component0"));
    }

    @Test
    void testLoadClassFromBaseClassLoaderBeforeSelf() throws Exception {
        ClassWorld world = new ClassWorld();

        ClassRealm base = world.newRealm("base");

        base.addURL(getJarUrl("a.jar"));

        ClassRealm child = world.newRealm("child", base);

        child.addURL(getJarUrl("a.jar"));

        Class<?> baseClass = loadClass(base, "a.A");
        Class<?> childClass = loadClass(child, "a.A");

        assertSame(base, baseClass.getClassLoader());
        assertSame(base, childClass.getClassLoader());
        assertSame(baseClass, childClass);
    }

    @Test
    void testLoadClassFromRealmWithCircularClassReferences() throws Exception {
        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);

        mainRealm.addURL(getJarUrl("circular-0.1.jar"));

        /*
         * This was reported to fail with a ClassCircularityError in IBM JDK 1.5.0-SR2, 1.5.0-SR7 and 1.6.0-SR2. It
         * works in IBM JDK 1.5.0-SR10 and 1.6.0-SR6.
         */
        loadClass(mainRealm, "A$C");
    }

    // ----------------------------------------------------------------------
    // Resource testing
    // ----------------------------------------------------------------------

    @Test
    void testResource() throws Exception {
        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);

        mainRealm.addURL(getJarUrl("component0-1.0.jar"));

        getResource(mainRealm, "META-INF/plexus/components.xml");
    }

    @Test
    void testMalformedResource() throws Exception {
        URL jarUrl = getJarUrl("component0-1.0.jar");

        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);

        mainRealm.addURL(jarUrl);

        ClassLoader officialClassLoader = new URLClassLoader(new URL[] {jarUrl});

        String resource = "META-INF/plexus/components.xml";

        assertNotNull(mainRealm.getResource(resource));
        assertNotNull(officialClassLoader.getResource(resource));

        /*
         * NOTE: Resource names with a leading slash are invalid when passed to a class loader and must not be found!
         * One can use a leading slash in Class.getResource() but not in ClassLoader.getResource().
         */

        assertSame(null, mainRealm.getResource("/" + resource));
        assertSame(null, officialClassLoader.getResource("/" + resource));
    }

    @Test
    void testFindResourceOnlyScansSelf() throws Exception {
        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);

        mainRealm.addURL(getJarUrl("a.jar"));

        ClassRealm childRealm = mainRealm.createChildRealm("child");

        childRealm.addURL(getJarUrl("b.jar"));

        assertNotNull(childRealm.getResource("a.properties"));
        assertNotNull(childRealm.getResource("b.properties"));

        assertNull(childRealm.findResource("a.properties"));

        assertNotNull(childRealm.findResource("b.properties"));
    }

    @Test
    void testFindResourcesOnlyScansSelf() throws Exception {
        ClassRealm mainRealm = new ClassRealm(new ClassWorld(), "main", null);

        mainRealm.addURL(getJarUrl("a.jar"));

        ClassRealm childRealm = mainRealm.createChildRealm("child");

        childRealm.addURL(getJarUrl("b.jar"));

        assertTrue(childRealm.getResources("a.properties").hasMoreElements());
        assertTrue(childRealm.getResources("b.properties").hasMoreElements());

        assertFalse(childRealm.findResources("a.properties").hasMoreElements());

        assertTrue(childRealm.findResources("b.properties").hasMoreElements());
    }

    /** Should never deadlock. Ever */
    @Test
    void testParallelDeadlockClassRealm() throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            doOneDeadlockAttempt();
        }
    }

    private void doOneDeadlockAttempt() throws InterruptedException {
        // Deadlock sample graciously ripped from http://docs.oracle.com/javase/7/docs/technotes/guides/lang/cl-mt.html
        final ClassRealm cl1 = new ClassRealm(new ClassWorld(), "cl1", null);
        final ClassRealm cl2 = new ClassRealm(new ClassWorld(), "cl2", cl1);
        cl1.setParentRealm(cl2);
        cl1.addURL(getJarUrl("deadlock.jar"));
        cl2.addURL(getJarUrl("deadlock.jar"));
        final CountDownLatch latch = new CountDownLatch(1);

        Runnable r1 = () -> {
            try {
                latch.await();
                cl1.loadClass("deadlock.A");
            } catch (ClassNotFoundException | InterruptedException e) {
                throw new RuntimeException(e);
            }
        };

        Runnable r2 = () -> {
            try {
                latch.await();
                cl1.loadClass("deadlock.C");
            } catch (ClassNotFoundException | InterruptedException e) {
                throw new RuntimeException(e);
            }
        };

        Thread thread = new Thread(r1);
        thread.start();
        Thread thread1 = new Thread(r2);
        thread1.start();
        latch.countDown();
        thread.join();
        thread1.join();
    }

    // ----------------------------------------------------------------------
    //
    // ----------------------------------------------------------------------

    private Class<?> loadClassOrNull(ClassRealm realm, String name) {
        try {
            return loadClass(realm, name);
        } catch (ClassNotFoundException e) {
            return null;
        }
    }

    private Class<?> loadClass(ClassRealm realm, String name) throws ClassNotFoundException {
        Class<?> cls = realm.loadClass(name);

        /*
         * NOTE: Load the class both directly from the realm and indirectly from an (ordinary) child class loader which
         * uses the specified class realm for parent delegation. The child class loader itself has no additional class
         * path entries but relies entirely on the provided class realm. Hence, the created child class loader should in
         * theory be able to load exactly the same classes/resources as the underlying class realm. In practice, it will
         * test that class realms properly integrate into the standard Java class loader hierarchy.
         */
        ClassLoader childLoader = new URLClassLoader(new URL[0], realm);
        assertEquals(cls, childLoader.loadClass(name));

        return cls;
    }

    private void getResource(ClassRealm realm, String name) throws Exception {
        ClassLoader childLoader = new URLClassLoader(new URL[0], realm);
        assertNotNull(realm.getResource(name));
        assertEquals(realm.getResource(name), childLoader.getResource(name));
        assertEquals(Collections.list(realm.getResources(name)), Collections.list(childLoader.getResources(name)));
    }
}