diff --git a/server/src/main/java/com/mirth/connect/server/userutil/ContextFactory.java b/server/src/main/java/com/mirth/connect/server/userutil/ContextFactory.java index c3aa5c0567..d7e2a90499 100644 --- a/server/src/main/java/com/mirth/connect/server/userutil/ContextFactory.java +++ b/server/src/main/java/com/mirth/connect/server/userutil/ContextFactory.java @@ -51,12 +51,18 @@ public ClassLoader getClassLoader() { } /** - * Returns a classloader containing only the libraries contained in the custom resources, with - * no parent classloader. If no custom libraries are being used in the current JavaScript - * context, this will return null. - * - * @return A classloader containing only the libraries contained in the custom resources, with - * no parent classloader. + * Returns a classloader containing only the libraries in the custom resources assigned to the + * current context. Use it to load classes from those libraries in isolation, for example a + * specific JDBC driver version, without interference from the versions the server itself ships. + *
+ * Core Java classes (for example {@code java.sql} or {@code javax.xml}) are visible through this + * classloader, but classes from the server or its plugins are not (its parent is + * {@link ClassLoader#getPlatformClassLoader()}). A class that exists in both a custom resource + * and the JDK resolves to the JDK's copy. The "Load Parent-First" option on a resource does not + * affect this classloader; it applies only to the classloader returned by {@link #getClassLoader()}. + * + * @return A classloader containing only the custom resource libraries, or null if the current + * context has no custom resources. */ public ClassLoader getIsolatedClassLoader() { return delegate.getIsolatedClassLoader(); diff --git a/server/src/main/java/com/mirth/connect/server/util/javascript/MirthContextFactory.java b/server/src/main/java/com/mirth/connect/server/util/javascript/MirthContextFactory.java index 5e303757d5..6ba3bb35f1 100644 --- a/server/src/main/java/com/mirth/connect/server/util/javascript/MirthContextFactory.java +++ b/server/src/main/java/com/mirth/connect/server/util/javascript/MirthContextFactory.java @@ -114,7 +114,7 @@ public ClassLoader getIsolatedClassLoader() { if (isolatedClassLoader == null && ArrayUtils.isNotEmpty(urls)) { synchronized (this) { if (isolatedClassLoader == null) { - isolatedClassLoader = new URLClassLoader(urls, null); + isolatedClassLoader = new URLClassLoader(urls, ClassLoader.getPlatformClassLoader()); } } } diff --git a/server/src/test/java/com/mirth/connect/server/util/javascript/MirthContextFactoryTest.java b/server/src/test/java/com/mirth/connect/server/util/javascript/MirthContextFactoryTest.java new file mode 100644 index 0000000000..b34ee8627d --- /dev/null +++ b/server/src/test/java/com/mirth/connect/server/util/javascript/MirthContextFactoryTest.java @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: Mirth Corporation + +package com.mirth.connect.server.util.javascript; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.net.URL; +import java.util.HashSet; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.mirth.connect.server.builders.JavaScriptBuilder; +import com.mirth.connect.server.controllers.CodeTemplateController; +import com.mirth.connect.server.controllers.ConfigurationController; +import com.mirth.connect.server.controllers.ControllerFactory; +import com.mirth.connect.server.controllers.EventController; +import com.mirth.connect.server.controllers.ExtensionController; + +public class MirthContextFactoryTest { + + @BeforeClass + public static void setUpBeforeClass() { + // Same mocked ControllerFactory pattern as JavaScriptUtilTest, so this class is + // self-sufficient regardless of which test classes ran (and injected) before it. + ControllerFactory controllerFactory = mock(ControllerFactory.class); + + EventController eventController = mock(EventController.class); + when(controllerFactory.createEventController()).thenReturn(eventController); + + ConfigurationController configurationController = mock(ConfigurationController.class); + when(controllerFactory.createConfigurationController()).thenReturn(configurationController); + + ExtensionController extensionController = mock(ExtensionController.class); + when(controllerFactory.createExtensionController()).thenReturn(extensionController); + + CodeTemplateController codeTemplateController = mock(CodeTemplateController.class); + when(controllerFactory.createCodeTemplateController()).thenReturn(codeTemplateController); + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + requestStaticInjection(ControllerFactory.class); + bind(ControllerFactory.class).toInstance(controllerFactory); + } + }); + injector.getInstance(ControllerFactory.class); + + JavaScriptBuilder.setControllersForTesting(extensionController, codeTemplateController); + } + + /* + * Regression test for #338: with a null parent, the isolated classloader cannot see java.sql + * on Java 9+, so custom driver resources failed to deploy. The parent must be the platform + * classloader: JRE classes visible, server classpath not. + */ + @Test + public void isolatedClassLoaderResolvesPlatformButNotServerClasses() throws Exception { + // This jar URL never gets read; it only exists because getIsolatedClassLoader() returns null on empty array. + URL dummyJar = new File("build/tmp/mirth-context-factory-test-dummy.jar").toURI().toURL(); + MirthContextFactory contextFactory = new MirthContextFactory(new URL[] { dummyJar }, new HashSet<>(), false); + + ClassLoader isolated = contextFactory.getIsolatedClassLoader(); + assertNotNull(isolated); + + // Fails with ClassNotFoundException if the parent ever goes back to null + isolated.loadClass("java.sql.Driver"); + + // Fails if the parent is ever widened to a loader that can see the server classpath + assertThrows(ClassNotFoundException.class, () -> isolated.loadClass(MirthContextFactory.class.getName())); + } + + @Test + public void isolatedClassLoaderIsNullWithoutResources() { + MirthContextFactory contextFactory = new MirthContextFactory(new URL[0], new HashSet<>(), false); + assertNull(contextFactory.getIsolatedClassLoader()); + } +}