diff --git a/jjava-jupyter/src/main/java/org/dflib/jjava/jupyter/kernel/BaseKernel.java b/jjava-jupyter/src/main/java/org/dflib/jjava/jupyter/kernel/BaseKernel.java index 0aa85be..0a2b747 100644 --- a/jjava-jupyter/src/main/java/org/dflib/jjava/jupyter/kernel/BaseKernel.java +++ b/jjava-jupyter/src/main/java/org/dflib/jjava/jupyter/kernel/BaseKernel.java @@ -547,10 +547,19 @@ protected void installDefaultExtensions() { installExtensionsFromClassLoader(getClass().getClassLoader()); } + /** + * Returns the parent ClassLoader to use when loading extensions from classpath. + * + * @return the parent ClassLoader for extension loading + */ + protected ClassLoader getParentClassLoader() { + return ClassLoader.getSystemClassLoader(); + } + /** * Locates, loads and initializes {@code Extension}s. Extension classes are discovered via {@link ServiceLoader}. - * It is passed a custom ClassLoader created internally based on a combination of system ClassLoader and the extra - * classpath specified as an argument. + * It is passed a custom ClassLoader created internally based on a combination of the parent ClassLoader + * (from {@link #getParentClassLoader()}) and the extra classpath specified as an argument. * * @param classpath one or more filesystem paths separated by {@link java.io.File#pathSeparator}. */ @@ -561,7 +570,8 @@ protected void installExtensionsFromClasspath(String classpath) { .map(BaseKernel::pathToURL) .toArray(URL[]::new); - try (URLClassLoader classLoader = URLClassLoader.newInstance(urls)) { + ClassLoader parentClassLoader = getParentClassLoader(); + try (URLClassLoader classLoader = new URLClassLoader(urls, parentClassLoader)) { installExtensionsFromClassLoader(classLoader); } catch (IOException e) { throw new RuntimeException(e); diff --git a/jjava-kernel/src/main/java/org/dflib/jjava/kernel/JavaKernel.java b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/JavaKernel.java index d81388d..00ae87b 100644 --- a/jjava-kernel/src/main/java/org/dflib/jjava/kernel/JavaKernel.java +++ b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/JavaKernel.java @@ -347,6 +347,11 @@ public void interrupt() { this.evaluator.interrupt(); } + @Override + protected ClassLoader getParentClassLoader() { + return evaluator.getJShellClassLoader(); + } + /** * @return a JShell instance used to evaluate Java code. */ diff --git a/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/CodeEvaluator.java b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/CodeEvaluator.java index d05e44b..38592e1 100644 --- a/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/CodeEvaluator.java +++ b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/CodeEvaluator.java @@ -177,7 +177,7 @@ private Object doEval(String code) { * Try to clean up information linked to a code snippet and the snippet itself */ private void dropSnippet(Snippet snippet) { - JJavaExecutionControl execControl = execControlProvider.getRegisteredControlByID(this.execControlID); + JJavaExecutionControl execControl = execControlProvider.getRegisteredControlByID(execControlID); shell.drop(snippet); // snippet.classFullName() returns name of a wrapper class created for a snippet String className = snippetClassName(snippet); @@ -272,4 +272,9 @@ public void interrupt() { execControl.interrupt(); } } + + public ClassLoader getJShellClassLoader() { + JJavaExecutionControl execControl = execControlProvider.getRegisteredControlByID(execControlID); + return execControl != null ? execControl.getJShellClassLoader() : null; + } } diff --git a/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/JJavaExecutionControl.java b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/JJavaExecutionControl.java index 47a0f72..51e4882 100644 --- a/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/JJavaExecutionControl.java +++ b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/JJavaExecutionControl.java @@ -179,6 +179,10 @@ void unloadClass(String className) { this.loaderDelegate.unloadClass(className); } + public ClassLoader getJShellClassLoader() { + return loaderDelegate.getClassLoader(); + } + @Override public String toString() { return "JJavaExecutionControl{" + diff --git a/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/JJavaLoaderDelegate.java b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/JJavaLoaderDelegate.java index 56eec3b..72987fd 100644 --- a/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/JJavaLoaderDelegate.java +++ b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/JJavaLoaderDelegate.java @@ -89,6 +89,10 @@ public void unloadClass(String name) { declaredClasses.remove(name); } + public ClassLoader getClassLoader() { + return classLoader; + } + class BytecodeClassLoader extends URLClassLoader { public BytecodeClassLoader() { diff --git a/jjava-kernel/src/test/java/org/dflib/jjava/kernel/JavaKernelExtensionsLifecycleTest.java b/jjava-kernel/src/test/java/org/dflib/jjava/kernel/JavaKernelExtensionsLifecycleTest.java index ac6a388..b9a9210 100644 --- a/jjava-kernel/src/test/java/org/dflib/jjava/kernel/JavaKernelExtensionsLifecycleTest.java +++ b/jjava-kernel/src/test/java/org/dflib/jjava/kernel/JavaKernelExtensionsLifecycleTest.java @@ -4,14 +4,14 @@ import org.junit.jupiter.api.Test; import java.nio.file.Path; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -public class JavaKernelExtensionsLifecycleTest { +class JavaKernelExtensionsLifecycleTest { @Test - public void defaultExtensions() { + void defaultExtension() { assertNull(JavaNotebookStatics.kernel); JavaKernel kernel = JavaKernel.builder().name("TestKernel").build(); @@ -28,16 +28,16 @@ public void defaultExtensions() { } @Test - void extraClasspathExtensions() throws Exception { - Path jar = TestJarFactory.buildJar( - "java/", - "java/org/dflib/jjava/kernel/test/TestExtension.java", - "java/META-INF/services/org.dflib.jjava.jupyter.Extension" + void extraClasspathExtension() throws Exception { + Path extensionJar = TestJarFactory.buildJar( + "extensions/classpath/", + "extensions/classpath/org/dflib/jjava/kernel/test/ExtraClasspathExtension.java", + "extensions/classpath/META-INF/services/org.dflib.jjava.jupyter.Extension" ); - String extraClasspath = PathsHandler.joinPaths(PathsHandler.resolveGlobs(jar.toAbsolutePath().toString())); + String extraClasspath = PathsHandler.joinPaths(List.of(extensionJar)); - String extInstalledProp = "ext.installs:org.dflib.jjava.kernel.test.TestExtension"; + String extInstalledProp = "ext.installs:org.dflib.jjava.kernel.test.ExtraClasspathExtension"; System.clearProperty(extInstalledProp); JavaKernel kernel = JavaKernel @@ -62,4 +62,63 @@ void extraClasspathExtensions() throws Exception { assertNull(System.getProperty(extInstalledProp)); } + @Test + void evalExtension() throws Exception { + Path extensionJar = TestJarFactory.buildJar( + "extensions/eval/", + "extensions/eval/org/dflib/jjava/kernel/test/EvalExtension.java", + "extensions/eval/META-INF/services/org.dflib.jjava.jupyter.Extension" + ); + + String extraClasspath = PathsHandler.joinPaths(List.of(extensionJar)); + + JavaKernel kernel = JavaKernel + .builder() + .name("TestKernel") + .build(); + try { + kernel.onStartup(); + kernel.addToClasspath(extraClasspath); + + Object installed = kernel.evalRaw("evalExtensionInstalled"); + assertEquals(true, installed, "EvalExtension should have been installed"); + + Object result = kernel.evalRaw("evalValue"); + assertEquals("Test message", result.toString(), "eval() call was not successful"); + } finally { + kernel.onShutdown(false); + } + } + + @Test + void libraryExtension() throws Exception { + Path libraryJar = TestJarFactory.buildJar( + "extensions/library/", + "extensions/library/org/dflib/jjava/kernel/test/TestLibraryClass.java" + ); + Path extensionJar = TestJarFactory.buildJar( + "extensions/library/", + "extensions/library/org/dflib/jjava/kernel/test/ExternalLibraryExtension.java", + "extensions/library/META-INF/services/org.dflib.jjava.jupyter.Extension" + ); + + String extraClasspath = PathsHandler.joinPaths(List.of(libraryJar, extensionJar)); + + JavaKernel kernel = JavaKernel + .builder() + .name("TestKernel") + .build(); + try { + kernel.onStartup(); + kernel.addToClasspath(extraClasspath); + + Object installed = kernel.evalRaw("externalLibraryExtensionInstalled"); + assertEquals(true, installed, "ExternalLibraryExtension should have been installed"); + + Object result = kernel.evalRaw("externalLibraryValue"); + assertEquals("Test message", result.toString(), "Library class method call was not successful"); + } finally { + kernel.onShutdown(false); + } + } } diff --git a/jjava-kernel/src/test/resources/extensions/classpath/META-INF/services/org.dflib.jjava.jupyter.Extension b/jjava-kernel/src/test/resources/extensions/classpath/META-INF/services/org.dflib.jjava.jupyter.Extension new file mode 100644 index 0000000..c275630 --- /dev/null +++ b/jjava-kernel/src/test/resources/extensions/classpath/META-INF/services/org.dflib.jjava.jupyter.Extension @@ -0,0 +1 @@ +org.dflib.jjava.kernel.test.ExtraClasspathExtension diff --git a/jjava-kernel/src/test/resources/java/org/dflib/jjava/kernel/test/TestExtension.java b/jjava-kernel/src/test/resources/extensions/classpath/org/dflib/jjava/kernel/test/ExtraClasspathExtension.java similarity index 87% rename from jjava-kernel/src/test/resources/java/org/dflib/jjava/kernel/test/TestExtension.java rename to jjava-kernel/src/test/resources/extensions/classpath/org/dflib/jjava/kernel/test/ExtraClasspathExtension.java index 968fd81..0fc927c 100644 --- a/jjava-kernel/src/test/resources/java/org/dflib/jjava/kernel/test/TestExtension.java +++ b/jjava-kernel/src/test/resources/extensions/classpath/org/dflib/jjava/kernel/test/ExtraClasspathExtension.java @@ -2,9 +2,8 @@ import org.dflib.jjava.jupyter.Extension; import org.dflib.jjava.jupyter.kernel.BaseKernel; -import org.dflib.jjava.kernel.JavaNotebookStatics; -public class TestExtension implements Extension { +public class ExtraClasspathExtension implements Extension { @Override public void install(BaseKernel kernel) { diff --git a/jjava-kernel/src/test/resources/extensions/eval/META-INF/services/org.dflib.jjava.jupyter.Extension b/jjava-kernel/src/test/resources/extensions/eval/META-INF/services/org.dflib.jjava.jupyter.Extension new file mode 100644 index 0000000..9c89d30 --- /dev/null +++ b/jjava-kernel/src/test/resources/extensions/eval/META-INF/services/org.dflib.jjava.jupyter.Extension @@ -0,0 +1 @@ +org.dflib.jjava.kernel.test.EvalExtension diff --git a/jjava-kernel/src/test/resources/extensions/eval/org/dflib/jjava/kernel/test/EvalExtension.java b/jjava-kernel/src/test/resources/extensions/eval/org/dflib/jjava/kernel/test/EvalExtension.java new file mode 100644 index 0000000..f901e33 --- /dev/null +++ b/jjava-kernel/src/test/resources/extensions/eval/org/dflib/jjava/kernel/test/EvalExtension.java @@ -0,0 +1,13 @@ +package org.dflib.jjava.kernel.test; + +import org.dflib.jjava.jupyter.Extension; +import org.dflib.jjava.jupyter.kernel.BaseKernel; + +public class EvalExtension implements Extension { + + @Override + public void install(BaseKernel kernel) { + kernel.eval("var evalValue = \"Test message\";"); + kernel.eval("var evalExtensionInstalled = true;"); + } +} diff --git a/jjava-kernel/src/test/resources/extensions/library/META-INF/services/org.dflib.jjava.jupyter.Extension b/jjava-kernel/src/test/resources/extensions/library/META-INF/services/org.dflib.jjava.jupyter.Extension new file mode 100644 index 0000000..8f4a2f3 --- /dev/null +++ b/jjava-kernel/src/test/resources/extensions/library/META-INF/services/org.dflib.jjava.jupyter.Extension @@ -0,0 +1 @@ +org.dflib.jjava.kernel.test.ExternalLibraryExtension diff --git a/jjava-kernel/src/test/resources/extensions/library/org/dflib/jjava/kernel/test/ExternalLibraryExtension.java b/jjava-kernel/src/test/resources/extensions/library/org/dflib/jjava/kernel/test/ExternalLibraryExtension.java new file mode 100644 index 0000000..8841aad --- /dev/null +++ b/jjava-kernel/src/test/resources/extensions/library/org/dflib/jjava/kernel/test/ExternalLibraryExtension.java @@ -0,0 +1,21 @@ +package org.dflib.jjava.kernel.test; + +import org.dflib.jjava.jupyter.Extension; +import org.dflib.jjava.jupyter.kernel.BaseKernel; + +public class ExternalLibraryExtension implements Extension { + + @Override + public void install(BaseKernel kernel) { + Object value; + try { + Class libClass = Class.forName("org.dflib.jjava.kernel.test.TestLibraryClass"); + value = libClass.getMethod("getMessage").invoke(null); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + + kernel.eval("var externalLibraryValue = \"" + value + "\";"); + kernel.eval("var externalLibraryExtensionInstalled = true;"); + } +} diff --git a/jjava-kernel/src/test/resources/extensions/library/org/dflib/jjava/kernel/test/TestLibraryClass.java b/jjava-kernel/src/test/resources/extensions/library/org/dflib/jjava/kernel/test/TestLibraryClass.java new file mode 100644 index 0000000..739876d --- /dev/null +++ b/jjava-kernel/src/test/resources/extensions/library/org/dflib/jjava/kernel/test/TestLibraryClass.java @@ -0,0 +1,8 @@ +package org.dflib.jjava.kernel.test; + +public class TestLibraryClass { + + public static String getMessage() { + return "Test message"; + } +} diff --git a/jjava-kernel/src/test/resources/java/META-INF/services/org.dflib.jjava.jupyter.Extension b/jjava-kernel/src/test/resources/java/META-INF/services/org.dflib.jjava.jupyter.Extension deleted file mode 100644 index dc621af..0000000 --- a/jjava-kernel/src/test/resources/java/META-INF/services/org.dflib.jjava.jupyter.Extension +++ /dev/null @@ -1 +0,0 @@ -org.dflib.jjava.kernel.test.TestExtension