diff --git a/jjava-kernel/pom.xml b/jjava-kernel/pom.xml index 24d10e4..8387de4 100644 --- a/jjava-kernel/pom.xml +++ b/jjava-kernel/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -30,4 +31,17 @@ test + + + + + org.apache.maven.plugins + maven-surefire-plugin + + --add-opens jdk.jshell/jdk.jshell=ALL-UNNAMED + false + + + + 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 d58b9b3..354b174 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 @@ -76,6 +76,7 @@ protected JavaKernel( MagicParser magicParser, MagicsRegistry magicsRegistry, ExtensionLoader extensionLoader, + String extraClasspath, boolean extensionsEnabled, StringStyler errorStyler, JShell jShell, @@ -101,6 +102,9 @@ protected JavaKernel( if (extensionsEnabled) { this.extensionLoader.loadFromDefaultClasspath().forEach(e -> e.install(this)); + if (extraClasspath != null) { + this.extensionLoader.loadFromClasspath(extraClasspath).forEach(e -> e.install(this)); + } } } @@ -383,6 +387,7 @@ public JavaKernel build() { buildMagicParser(magicTranspiler), buildMagicsRegistry(), buildExtensionLoader(), + buildExtraClasspath(), buildExtensionsEnabled(), buildErrorStyler(), jShell, @@ -396,5 +401,9 @@ protected List buildHelpLinks() { new HelpLink("JJava homepage", "https://github.com/dflib/jjava") ); } + + private String buildExtraClasspath() { + return extraClasspath; + } } } diff --git a/jjava-kernel/src/test/java/org/dflib/jjava/kernel/ExtensionLoadingBuiltInTest.java b/jjava-kernel/src/test/java/org/dflib/jjava/kernel/ExtensionLoadingBuiltInTest.java new file mode 100644 index 0000000..a30cf45 --- /dev/null +++ b/jjava-kernel/src/test/java/org/dflib/jjava/kernel/ExtensionLoadingBuiltInTest.java @@ -0,0 +1,14 @@ +package org.dflib.jjava.kernel; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class ExtensionLoadingBuiltInTest { + + @Test + void loadsExtensionsFromInitialClasspath() { + JavaKernel.builder().name("TestKernel").build(); + assertNotNull(JavaNotebookStatics.kernel(), "Built-in extension should be installed from initial classpath"); + } +} diff --git a/jjava-kernel/src/test/java/org/dflib/jjava/kernel/ExtensionLoadingExtraClasspathTest.java b/jjava-kernel/src/test/java/org/dflib/jjava/kernel/ExtensionLoadingExtraClasspathTest.java new file mode 100644 index 0000000..67a73ce --- /dev/null +++ b/jjava-kernel/src/test/java/org/dflib/jjava/kernel/ExtensionLoadingExtraClasspathTest.java @@ -0,0 +1,36 @@ +package org.dflib.jjava.kernel; + +import org.dflib.jjava.jupyter.kernel.util.PathsHandler; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ExtensionLoadingExtraClasspathTest { + + @Test + void loadsExtensionsFromExtraClasspath() throws Exception { + Path jar = TestJarFactory.buildJar( + "java/", + "java/org/dflib/jjava/kernel/test/TestExtension.java", + "java/META-INF/services/org.dflib.jjava.jupyter.Extension" + ); + + String extraClasspath = PathsHandler.joinPaths(PathsHandler.resolveGlobs(jar.toAbsolutePath().toString())); + + String extensionClassName = "org.dflib.jjava.kernel.test.TestExtension"; + String installationsProperty = extensionInstallationsProperty(extensionClassName); + System.clearProperty(installationsProperty); + + JavaKernel kernel = JavaKernel.builder().name("TestKernel").extraClasspath(extraClasspath).build(); + assertEquals("1", System.getProperty(installationsProperty)); + + kernel.addToClasspath(extraClasspath); + assertEquals("1", System.getProperty(installationsProperty)); + } + + private static String extensionInstallationsProperty(String className) { + return "ext.installs:" + className; + } +} diff --git a/jjava-kernel/src/test/java/org/dflib/jjava/kernel/TestJarFactory.java b/jjava-kernel/src/test/java/org/dflib/jjava/kernel/TestJarFactory.java new file mode 100644 index 0000000..11fadae --- /dev/null +++ b/jjava-kernel/src/test/java/org/dflib/jjava/kernel/TestJarFactory.java @@ -0,0 +1,119 @@ +package org.dflib.jjava.kernel; + +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +final class TestJarFactory { + + private static final Map, Path> cache = new HashMap<>(); + + private TestJarFactory() { + } + + static synchronized Path buildJar(String prefix, String... resourcePaths) throws Exception { + Set cacheKey = Set.of(resourcePaths); + if (cache.containsKey(cacheKey)) { + Path cached = cache.get(cacheKey); + if (Files.exists(cached)) { + return cached; + } + } + + Path tmpDir = Files.createTempDirectory("jjava-test-jar-"); + Path srcDir = tmpDir.resolve("src"); + Path classesDir = tmpDir.resolve("classes"); + Files.createDirectories(srcDir); + Files.createDirectories(classesDir); + + moveResources(prefix, resourcePaths, srcDir, classesDir); + compileJava(srcDir, classesDir); + Path jar = packageJar(tmpDir.resolve("test.jar"), classesDir); + + cache.put(cacheKey, jar); + return jar; + } + + private static void moveResources(String prefix, String[] resourcePaths, Path srcDir, Path classesDir) throws IOException { + for (String resourcePath : resourcePaths) { + URL url = TestJarFactory.class.getResource("/" + resourcePath); + if (url == null) { + throw new IllegalArgumentException("Resource not found: " + resourcePath); + } + + String targetRelativePath = resourcePath; + if (prefix != null && !prefix.isEmpty() && resourcePath.startsWith(prefix)) { + targetRelativePath = resourcePath.substring(prefix.length()); + } + + Path targetDir = resourcePath.endsWith(".java") ? srcDir : classesDir; + Path targetPath = targetDir.resolve(targetRelativePath); + Files.createDirectories(targetPath.getParent()); + try (InputStream in = url.openStream()) { + Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING); + } + } + } + + private static void compileJava(Path srcDir, Path classesDir) throws IOException { + List srcFiles; + try (Stream stream = Files.walk(srcDir)) { + srcFiles = stream.filter(Files::isRegularFile).map(Path::toFile).collect(Collectors.toList()); + } + if (srcFiles.isEmpty()) { + return; + } + + JavaCompiler compiler = Objects.requireNonNull(ToolProvider.getSystemJavaCompiler()); + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + try (StandardJavaFileManager fm = compiler.getStandardFileManager(diagnostics, null, null)) { + fm.setLocation(StandardLocation.CLASS_OUTPUT, List.of(classesDir.toFile())); + Iterable units = fm.getJavaFileObjectsFromFiles(srcFiles); + Boolean ok = compiler.getTask(null, fm, diagnostics, null, null, units).call(); + if (!Boolean.TRUE.equals(ok)) { + throw new IllegalStateException("Compilation failed: " + diagnostics.getDiagnostics()); + } + } + } + + private static Path packageJar(Path jarPath, Path classesDir) throws IOException { + try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(jarPath))) { + writeDirToJar(jos, classesDir, classesDir); + } + return jarPath; + } + + private static void writeDirToJar(JarOutputStream jos, Path root, Path dir) throws IOException { + try (Stream stream = Files.walk(dir)) { + for (Path path : (Iterable) stream::iterator) { + if (Files.isDirectory(path)) { + continue; + } + String entryName = root.relativize(path).toString().replace('\\', '/'); + JarEntry entry = new JarEntry(entryName); + jos.putNextEntry(entry); + Files.copy(path, jos); + jos.closeEntry(); + } + } + } +} 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 new file mode 100644 index 0000000..dc621af --- /dev/null +++ b/jjava-kernel/src/test/resources/java/META-INF/services/org.dflib.jjava.jupyter.Extension @@ -0,0 +1 @@ +org.dflib.jjava.kernel.test.TestExtension diff --git a/jjava-kernel/src/test/resources/java/org/dflib/jjava/kernel/test/TestExtension.java b/jjava-kernel/src/test/resources/java/org/dflib/jjava/kernel/test/TestExtension.java new file mode 100644 index 0000000..9be9422 --- /dev/null +++ b/jjava-kernel/src/test/resources/java/org/dflib/jjava/kernel/test/TestExtension.java @@ -0,0 +1,19 @@ +package org.dflib.jjava.kernel.test; + +import org.dflib.jjava.jupyter.Extension; +import org.dflib.jjava.jupyter.kernel.BaseKernel; + +public class TestExtension implements Extension { + + @Override + public void install(BaseKernel kernel) { + String key = "ext.installs:" + getClass().getName(); + String value = System.getProperty(key, "0"); + try { + int count = Integer.parseInt(value); + System.setProperty(key, String.valueOf(count + 1)); + } catch (NumberFormatException e) { + System.setProperty(key, "1"); + } + } +}