ClassRealmImplTest.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;

import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

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

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

class ClassRealmImplTest extends AbstractClassWorldsTestCase {
    private ClassWorld world;

    @BeforeEach
    public void setUp() {
        this.world = new ClassWorld();
    }

    @AfterEach
    public void tearDown() {
        this.world = null;
    }

    @Test
    void testNewRealm() throws Exception {
        ClassRealm realm = this.world.newRealm("foo");
        assertNotNull(realm);
        assertSame(this.world, realm.getWorld());
        assertEquals("foo", realm.getId());
    }

    @Test
    void testLocateSourceRealmNoImports() {
        ClassRealm realm = new ClassRealm(this.world, "foo", null);
        assertSame(null, realm.getImportClassLoader("com.werken.Stuff"));
    }

    @Test
    void testLocateSourceRealmSimpleImport() throws Exception {
        ClassRealm mainRealm = this.world.newRealm("main");
        ClassRealm werkflowRealm = this.world.newRealm("werkflow");
        mainRealm.importFrom("werkflow", "com.werken.werkflow");

        assertSame(werkflowRealm, mainRealm.getImportClassLoader("com.werken.werkflow.WerkflowEngine"));
        assertSame(werkflowRealm, mainRealm.getImportClassLoader("com/werken/werkflow/some.properties"));
        assertSame(werkflowRealm, mainRealm.getImportClassLoader("com.werken.werkflow.process.ProcessManager"));
        assertSame(null, mainRealm.getImportClassLoader("com.werken.blissed.Process"));
        assertSame(null, mainRealm.getImportClassLoader("java.lang.Object"));
        assertSame(null, mainRealm.getImportClassLoader("NoviceProgrammerClass"));
    }

    @Test
    void testLocateSourceRealmMultipleImport() throws Exception {
        ClassRealm mainRealm = this.world.newRealm("main");
        ClassRealm werkflowRealm = this.world.newRealm("werkflow");
        ClassRealm blissedRealm = this.world.newRealm("blissed");
        mainRealm.importFrom("werkflow", "com.werken.werkflow");
        mainRealm.importFrom("blissed", "com.werken.blissed");

        assertSame(werkflowRealm, mainRealm.getImportClassLoader("com.werken.werkflow.WerkflowEngine"));
        assertSame(werkflowRealm, mainRealm.getImportClassLoader("com.werken.werkflow.process.ProcessManager"));
        assertSame(blissedRealm, mainRealm.getImportClassLoader("com.werken.blissed.Process"));
        assertSame(blissedRealm, mainRealm.getImportClassLoader("com.werken.blissed.guard.BooleanGuard"));
        assertSame(null, mainRealm.getImportClassLoader("java.lang.Object"));
        assertSame(null, mainRealm.getImportClassLoader("NoviceProgrammerClass"));
    }

    @Test
    void testLocateSourceRealmHierachy() throws Exception {
        ClassRealm mainRealm = this.world.newRealm("main");
        ClassRealm fooRealm = this.world.newRealm("foo");
        ClassRealm fooBarRealm = this.world.newRealm("fooBar");
        ClassRealm fooBarBazRealm = this.world.newRealm("fooBarBaz");
        mainRealm.importFrom("foo", "foo");
        mainRealm.importFrom("fooBar", "foo.bar");
        mainRealm.importFrom("fooBarBaz", "foo.bar.baz");

        assertSame(fooRealm, mainRealm.getImportClassLoader("foo.Goober"));
        assertSame(fooRealm, mainRealm.getImportClassLoader("foo.cheese.Goober"));
        assertSame(fooBarRealm, mainRealm.getImportClassLoader("foo.bar.Goober"));
        assertSame(fooBarRealm, mainRealm.getImportClassLoader("foo.bar.cheese.Goober"));
        assertSame(fooBarBazRealm, mainRealm.getImportClassLoader("foo.bar.baz.Goober"));
        assertSame(fooBarBazRealm, mainRealm.getImportClassLoader("foo.bar.baz.cheese.Goober"));
        assertSame(null, mainRealm.getImportClassLoader("java.lang.Object"));
        assertSame(null, mainRealm.getImportClassLoader("NoviceProgrammerClass"));
    }

    @Test
    void testLocateSourceRealmHierachyReverse() throws Exception {
        ClassRealm fooBarBazRealm = this.world.newRealm("fooBarBaz");
        ClassRealm fooBarRealm = this.world.newRealm("fooBar");
        ClassRealm fooRealm = this.world.newRealm("foo");
        ClassRealm mainRealm = this.world.newRealm("main");
        mainRealm.importFrom("fooBarBaz", "foo.bar.baz");
        mainRealm.importFrom("fooBar", "foo.bar");
        mainRealm.importFrom("foo", "foo");

        assertSame(fooRealm, mainRealm.getImportClassLoader("foo.Goober"));
        assertSame(fooRealm, mainRealm.getImportClassLoader("foo.cheese.Goober"));
        assertSame(fooBarRealm, mainRealm.getImportClassLoader("foo.bar.Goober"));
        assertSame(fooBarRealm, mainRealm.getImportClassLoader("foo.bar.cheese.Goober"));
        assertSame(fooBarBazRealm, mainRealm.getImportClassLoader("foo.bar.baz.Goober"));
        assertSame(fooBarBazRealm, mainRealm.getImportClassLoader("foo.bar.baz.cheese.Goober"));
        assertSame(null, mainRealm.getImportClassLoader("java.lang.Object"));
        assertSame(null, mainRealm.getImportClassLoader("NoviceProgrammerClass"));
    }

    @Test
    void testLoadClassSystemClass() throws Exception {
        ClassRealm mainRealm = this.world.newRealm("main");
        Class<?> cls = mainRealm.loadClass("java.lang.Object");
        assertNotNull(cls);
    }

    @Test
    void testLoadClassNonSystemClass() throws Exception {
        ClassRealm mainRealm = this.world.newRealm("main");
        try {
            Class<?> c = mainRealm.loadClass("com.werken.projectz.UberThing");
            System.out.println("c = " + c);
            fail("A ClassNotFoundException should be thrown!");
        } catch (ClassNotFoundException e) {
            // expected and correct
        }
    }

    @Test
    void testLoadClassClassWorldsClass() throws Exception {
        ClassRealm mainRealm = this.world.newRealm("main");
        Class<?> cls = mainRealm.loadClass("org.codehaus.plexus.classworlds.ClassWorld");
        assertNotNull(cls);
        assertSame(ClassWorld.class, cls);
    }

    @Test
    void testLoadClassLocal() throws Exception {
        ClassRealm mainRealm = this.world.newRealm("main");
        try {
            mainRealm.loadClass("a.A");
        } catch (ClassNotFoundException e) {
            // expected and correct
        }

        mainRealm.addURL(getJarUrl("a.jar"));
        Class<?> classA = mainRealm.loadClass("a.A");
        assertNotNull(classA);

        ClassRealm otherRealm = this.world.newRealm("other");
        try {
            otherRealm.loadClass("a.A");
        } catch (ClassNotFoundException e) {
            // expected and correct
        }
    }

    @Test
    void testLoadClassImported() throws Exception {
        ClassRealm mainRealm = this.world.newRealm("main");
        ClassRealm realmA = this.world.newRealm("realmA");

        try {
            realmA.loadClass("a.A");
            fail("realmA.loadClass(a.A) should have thrown a ClassNotFoundException");
        } catch (ClassNotFoundException e) {
            // expected and correct
        }

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

        try {
            mainRealm.loadClass("a.A");
            fail("mainRealm.loadClass(a.A) should have thrown a ClassNotFoundException");
        } catch (ClassNotFoundException e) {
            // expected and correct
        }

        mainRealm.importFrom("realmA", "a");

        Class<?> classA = realmA.loadClass("a.A");
        assertNotNull(classA);
        assertEquals(realmA, classA.getClassLoader());

        Class<?> classMain = mainRealm.loadClass("a.A");
        assertNotNull(classMain);
        assertEquals(realmA, classMain.getClassLoader());
        assertSame(classA, classMain);
    }

    @Test
    void testLoadClassPackage() throws Exception {
        ClassRealm realmA = this.world.newRealm("realmA");
        realmA.addURL(getJarUrl("a.jar"));

        Class<?> clazz = realmA.loadClass("a.A");
        assertNotNull(clazz);
        assertEquals("a.A", clazz.getName());

        Package p = clazz.getPackage();
        assertNotNull(p);
        assertEquals("a", p.getName(), "p.getName()");
    }

    @Test
    void testLoadClassComplex() throws Exception {
        ClassRealm realmA = this.world.newRealm("realmA");
        ClassRealm realmB = this.world.newRealm("realmB");
        ClassRealm realmC = this.world.newRealm("realmC");
        realmA.addURL(getJarUrl("a.jar"));
        realmB.addURL(getJarUrl("b.jar"));
        realmC.addURL(getJarUrl("c.jar"));
        realmC.importFrom("realmA", "a");
        realmC.importFrom("realmB", "b");
        realmA.importFrom("realmC", "c");

        Class<?> classAA = realmA.loadClass("a.A");
        Class<?> classBB = realmB.loadClass("b.B");
        Class<?> classCC = realmC.loadClass("c.C");
        assertNotNull(classAA);
        assertNotNull(classBB);
        assertNotNull(classCC);
        assertEquals(realmA, classAA.getClassLoader());
        assertEquals(realmB, classBB.getClassLoader());
        assertEquals(realmC, classCC.getClassLoader());

        // load from C
        Class<?> classAC = realmC.loadClass("a.A");
        assertNotNull(classAC);
        assertSame(classAA, classAC);
        assertEquals(realmA, classAC.getClassLoader());

        Class<?> classBC = realmC.loadClass("b.B");
        assertNotNull(classBC);
        assertSame(classBB, classBC);
        assertEquals(realmB, classBC.getClassLoader());

        // load from A
        Class<?> classCA = realmA.loadClass("c.C");
        assertNotNull(classCA);
        assertSame(classCC, classCA);
        assertEquals(realmC, classCA.getClassLoader());

        try {
            realmA.loadClass("b.B");
            fail("throw ClassNotFoundException");
        } catch (ClassNotFoundException e) {
            // expected and correct
        }

        // load from B
        try {
            realmB.loadClass("a.A");
            fail("throw ClassNotFoundException");
        } catch (ClassNotFoundException e) {
            // expected and correct
        }

        try {
            realmB.loadClass("c.C");
            fail("throw ClassNotFoundException");
        } catch (ClassNotFoundException e) {
            // expected and correct
        }
    }

    @Test
    void testLoadClassClassWorldsClassRepeatedly() throws Exception {
        ClassRealm mainRealm = this.world.newRealm("main");
        for (int i = 0; i < 100; i++) {
            Class<?> cls = mainRealm.loadClass("org.codehaus.plexus.classworlds.ClassWorld");
            assertNotNull(cls);
            assertSame(ClassWorld.class, cls);
        }
    }

    @Test
    void testLoadClassWithModuleNameJava9() {
        final ExtendedClassRealm mainRealm = new ExtendedClassRealm(world);
        mainRealm.addURL(getJarUrl("a.jar"));
        assertNotNull(mainRealm.simulateLoadClassFromModule("a.A"));
    }

    @Test
    void testGetResourcesBaseBeforeSelf() throws Exception {
        String resource = "common.properties";

        ClassRealm base = this.world.newRealm("realmA");
        base.addURL(getJarUrl("a.jar"));
        URL baseUrl = base.getResource(resource);
        assertNotNull(baseUrl);

        ClassRealm sub = this.world.newRealm("realmB", base);
        sub.addURL(getJarUrl("b.jar"));
        URL subUrl = sub.getResource(resource);
        assertNotNull(subUrl);
        assertEquals(baseUrl, subUrl);

        List<String> urls = new ArrayList<>();
        for (URL url : Collections.list(sub.getResources(resource))) {
            String path = url.toString();
            path = path.substring(path.lastIndexOf('/', path.lastIndexOf(".jar!")));
            urls.add(path);
        }
        assertEquals(Arrays.asList("/a.jar!/common.properties", "/b.jar!/common.properties"), urls);
    }

    @Test
    void testGetResourcesSelfBeforeParent() throws Exception {
        String resource = "common.properties";

        ClassRealm parent = this.world.newRealm("realmA");
        parent.addURL(getJarUrl("a.jar"));
        URL parentUrl = parent.getResource(resource);
        assertNotNull(parentUrl);

        ClassRealm child = parent.createChildRealm("realmB");
        child.addURL(getJarUrl("b.jar"));
        URL childUrl = child.getResource(resource);
        assertNotNull(childUrl);

        List<URL> urls = Collections.list(child.getResources(resource));
        assertNotNull(urls);
        assertEquals(Arrays.asList(childUrl, parentUrl), urls);
    }

    /**
     * Simulates new {@code java.lang.ClassLoader#findClass(String,String)} introduced with Java 9.
     * It is reversed in terms of inheritance but enables to simulate the same behavior in these tests.
     * @see <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ClassLoader.html#findClass(java.lang.String,java.lang.String)">ClassLoader#findClass(String,String)</a>
     */
    private static class ExtendedClassRealm extends ClassRealm {

        ExtendedClassRealm(final ClassWorld world) {
            super(world, "java9", Thread.currentThread().getContextClassLoader());
        }

        public Class<?> simulateLoadClassFromModule(final String name) {
            synchronized (getClassLoadingLock(name)) {
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    c = findClass(null, name);
                }
                return c;
            }
        }
    }
}