prefixes();
+
+ @Override
+ public final boolean matches(Path path) {
+ if (prefixes().contains(path)) {
+ return true;
+ }
+ // Note: we could make a more efficient impl w/ a tree-based approach based on the names
+ for (Path prefix : prefixes()) {
+ if (path.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Check
+ final void checkPrefixesNonEmpty() {
+ if (prefixes().isEmpty()) {
+ throw new IllegalArgumentException("prefixes must be non-empty");
+ }
+ }
+
+ interface Builder {
+ Builder addPrefixes(Path element);
+
+ Builder addPrefixes(Path... elements);
+
+ Builder addAllPrefixes(Iterable extends Path> elements);
+
+ PathsPrefixes build();
+ }
+}
diff --git a/plugin/src/main/java/io/deephaven/plugin/js/package-info.java b/plugin/src/main/java/io/deephaven/plugin/js/package-info.java
new file mode 100644
index 00000000000..3f3ee3f3101
--- /dev/null
+++ b/plugin/src/main/java/io/deephaven/plugin/js/package-info.java
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+
+/**
+ * The Deephaven server supports {@link io.deephaven.plugin.js.JsPlugin JS plugins} which allow custom javascript (and
+ * related content) to be served under the HTTP path "js-plugins/".
+ *
+ *
+ * A "js-plugins/manifest.json" is served that allows clients to discover what JS plugins are installed. This will be a
+ * JSON object, and will have a "plugins" array, with object elements that have a "name", "version", and "main". All
+ * files served via a specific plugin will be accessed under "js-plugins/{name}/". The main entry file for a plugin will
+ * be accessed at "js-plugins/{name}/{main}". The "version" is currently for informational purposes only.
+ *
+ * @see deephaven-plugins for Deephaven-maintained JS
+ * plugins
+ */
+package io.deephaven.plugin.js;
diff --git a/py/server/deephaven_internal/plugin/js/__init__.py b/py/server/deephaven_internal/plugin/js/__init__.py
new file mode 100644
index 00000000000..b0f18f3a8da
--- /dev/null
+++ b/py/server/deephaven_internal/plugin/js/__init__.py
@@ -0,0 +1,29 @@
+#
+# Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+#
+
+import jpy
+import pathlib
+
+from deephaven.plugin.js import JsPlugin
+
+_JJsPlugin = jpy.get_type("io.deephaven.plugin.js.JsPlugin")
+_JPath = jpy.get_type("java.nio.file.Path")
+
+
+def to_j_js_plugin(js_plugin: JsPlugin) -> jpy.JType:
+ path = js_plugin.path()
+ if not isinstance(path, pathlib.Path):
+ # Adding a little bit of extra safety for this version of the server.
+ # There's potential that the return type of JsPlugin.path expands in the future.
+ raise Exception(
+ f"Expecting pathlib.Path, is type(js_plugin.path())={type(path)}, js_plugin={js_plugin}"
+ )
+ j_path = _JPath.of(str(path))
+ main_path = j_path.relativize(j_path.resolve(js_plugin.main))
+ builder = _JJsPlugin.builder()
+ builder.name(js_plugin.name)
+ builder.version(js_plugin.version)
+ builder.main(main_path)
+ builder.path(j_path)
+ return builder.build()
diff --git a/py/server/deephaven_internal/plugin/register.py b/py/server/deephaven_internal/plugin/register.py
index a35d152c91c..91ec3c20e4e 100644
--- a/py/server/deephaven_internal/plugin/register.py
+++ b/py/server/deephaven_internal/plugin/register.py
@@ -8,9 +8,11 @@
from typing import Union, Type
from deephaven.plugin import Plugin, Registration, Callback
from deephaven.plugin.object_type import ObjectType
+from deephaven.plugin.js import JsPlugin
from .object import ObjectTypeAdapter
+from .js import to_j_js_plugin
-_JCallbackAdapter = jpy.get_type('io.deephaven.server.plugin.python.CallbackAdapter')
+_JCallbackAdapter = jpy.get_type("io.deephaven.server.plugin.python.CallbackAdapter")
def initialize_all_and_register_into(callback: _JCallbackAdapter):
@@ -20,6 +22,7 @@ def initialize_all_and_register_into(callback: _JCallbackAdapter):
class RegistrationAdapter(Callback):
"""Python implementation of Callback that delegates to its Java counterpart."""
+
def __init__(self, callback: _JCallbackAdapter):
self._callback = callback
@@ -29,8 +32,10 @@ def register(self, plugin: Union[Plugin, Type[Plugin]]):
plugin = plugin()
if isinstance(plugin, ObjectType):
self._callback.registerObjectType(plugin.name, ObjectTypeAdapter(plugin))
+ elif isinstance(plugin, JsPlugin):
+ self._callback.registerJsPlugin(to_j_js_plugin(plugin))
else:
- raise NotImplementedError
+ raise NotImplementedError(f"Unexpected type: {type(plugin)}")
def __str__(self):
return str(self._callback)
diff --git a/py/server/setup.py b/py/server/setup.py
index df83ea7e498..4161000e0d4 100644
--- a/py/server/setup.py
+++ b/py/server/setup.py
@@ -56,7 +56,7 @@ def _compute_version():
python_requires='>=3.8',
install_requires=[
'jpy>=0.14.0',
- 'deephaven-plugin==0.5.0',
+ 'deephaven-plugin>=0.6.0',
'numpy',
'pandas>=1.5.0',
'pyarrow',
diff --git a/server/jetty/src/main/java/io/deephaven/server/jetty/CopyHelper.java b/server/jetty/src/main/java/io/deephaven/server/jetty/CopyHelper.java
index 8b13de748a6..ccd6ab2f802 100644
--- a/server/jetty/src/main/java/io/deephaven/server/jetty/CopyHelper.java
+++ b/server/jetty/src/main/java/io/deephaven/server/jetty/CopyHelper.java
@@ -4,40 +4,77 @@
package io.deephaven.server.jetty;
import java.io.IOException;
+import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.PathMatcher;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Objects;
class CopyHelper {
- static void copyRecursive(Path src, Path dst) throws IOException {
+ static void copyRecursive(Path src, Path dst, PathMatcher pathMatcher) throws IOException {
+ copyRecursive(src, dst, pathMatcher, d -> true);
+ }
+
+ static void copyRecursive(Path src, Path dst, PathMatcher pathMatcher, PathMatcher dirMatcher) throws IOException {
Files.createDirectories(dst.getParent());
- Files.walkFileTree(src, new CopyRecursiveVisitor(src, dst));
+ Files.walkFileTree(src, new CopyRecursiveVisitor(src, dst, pathMatcher, dirMatcher));
}
private static class CopyRecursiveVisitor extends SimpleFileVisitor {
private final Path src;
private final Path dst;
+ private final PathMatcher pathMatcher;
+ private final PathMatcher dirMatcher;
- public CopyRecursiveVisitor(Path src, Path dst) {
+ public CopyRecursiveVisitor(Path src, Path dst, PathMatcher pathMatcher, PathMatcher dirMatcher) {
this.src = Objects.requireNonNull(src);
this.dst = Objects.requireNonNull(dst);
+ this.pathMatcher = Objects.requireNonNull(pathMatcher);
+ this.dirMatcher = Objects.requireNonNull(dirMatcher);
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
- // Note: toString() necessary for src/dst that don't share the same root FS
- Files.copy(dir, dst.resolve(src.relativize(dir).toString()), StandardCopyOption.COPY_ATTRIBUTES);
- return FileVisitResult.CONTINUE;
+ final Path relativeDir = src.relativize(dir);
+ if (dirMatcher.matches(relativeDir) || pathMatcher.matches(relativeDir)) {
+ // Note: toString() necessary for src/dst that don't share the same root FS
+ Files.copy(dir, dst.resolve(relativeDir.toString()), StandardCopyOption.COPY_ATTRIBUTES);
+ return FileVisitResult.CONTINUE;
+ }
+ return FileVisitResult.SKIP_SUBTREE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
- // Note: toString() necessary for src/dst that don't share the same root FS
- Files.copy(file, dst.resolve(src.relativize(file).toString()), StandardCopyOption.COPY_ATTRIBUTES);
+ final Path relativeFile = src.relativize(file);
+ if (pathMatcher.matches(relativeFile)) {
+ // Note: toString() necessary for src/dst that don't share the same root FS
+ Files.copy(file, dst.resolve(relativeFile.toString()), StandardCopyOption.COPY_ATTRIBUTES);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ if (exc != null) {
+ throw exc;
+ }
+ final Path relativeDir = src.relativize(dir);
+ if (!pathMatcher.matches(relativeDir)) {
+ // If the specific dir does not match as a path (even if it _did_ match as a directory), we
+ // "optimistically" try and delete it; if the directory is not empty (b/c some subpath matched and was
+ // copied), the delete will fail. (We could have an alternative impl that keeps track w/ a stack if any
+ // subpaths matched.)
+ try {
+ Files.delete(dir);
+ } catch (DirectoryNotEmptyException e) {
+ // ignore
+ }
+ }
return FileVisitResult.CONTINUE;
}
}
diff --git a/server/jetty/src/main/java/io/deephaven/server/jetty/JsPluginManifest.java b/server/jetty/src/main/java/io/deephaven/server/jetty/JsPluginManifest.java
deleted file mode 100644
index 6b7f162834e..00000000000
--- a/server/jetty/src/main/java/io/deephaven/server/jetty/JsPluginManifest.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
- */
-package io.deephaven.server.jetty;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import io.deephaven.annotations.SimpleStyle;
-import org.immutables.value.Value.Immutable;
-import org.immutables.value.Value.Parameter;
-
-import java.util.List;
-
-@Immutable
-@SimpleStyle
-abstract class JsPluginManifest {
- public static final String PLUGINS = "plugins";
-
- @JsonCreator
- public static JsPluginManifest of(
- @JsonProperty(value = PLUGINS, required = true) List plugins) {
- return ImmutableJsPluginManifest.of(plugins);
- }
-
- @Parameter
- @JsonProperty(PLUGINS)
- public abstract List plugins();
-}
diff --git a/server/jetty/src/main/java/io/deephaven/server/jetty/JsPlugins.java b/server/jetty/src/main/java/io/deephaven/server/jetty/JsPlugins.java
index ffebf7b1700..41f1527c52a 100644
--- a/server/jetty/src/main/java/io/deephaven/server/jetty/JsPlugins.java
+++ b/server/jetty/src/main/java/io/deephaven/server/jetty/JsPlugins.java
@@ -4,19 +4,12 @@
package io.deephaven.server.jetty;
import io.deephaven.plugin.js.JsPlugin;
-import io.deephaven.plugin.js.JsPluginManifestPath;
-import io.deephaven.plugin.js.JsPluginPackagePath;
import io.deephaven.plugin.js.JsPluginRegistration;
import java.io.IOException;
-import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URI;
-import java.nio.file.Files;
import java.util.Objects;
-import java.util.function.Consumer;
-
-import static io.deephaven.server.jetty.Json.OBJECT_MAPPER;
/**
* Jetty-specific implementation of {@link JsPluginRegistration} to collect plugins and advertise their contents to
@@ -42,56 +35,9 @@ public URI filesystem() {
@Override
public void register(JsPlugin jsPlugin) {
try {
- if (jsPlugin instanceof JsPluginPackagePath) {
- copy((JsPluginPackagePath) jsPlugin, zipFs);
- return;
- }
- if (jsPlugin instanceof JsPluginManifestPath) {
- copyAll((JsPluginManifestPath) jsPlugin, zipFs);
- return;
- }
+ zipFs.add(jsPlugin);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
- throw new IllegalStateException("Unexpected JsPlugin class: " + jsPlugin.getClass());
- }
-
- private static void copy(JsPluginPackagePath srcPackagePath, JsPluginsZipFilesystem dest)
- throws IOException {
- copy(srcPackagePath, dest, null);
- }
-
- private static void copy(JsPluginPackagePath srcPackagePath, JsPluginsZipFilesystem dest,
- JsPluginManifestEntry expected)
- throws IOException {
- final JsPluginManifestEntry srcEntry = entry(srcPackagePath);
- if (expected != null && !expected.equals(srcEntry)) {
- throw new IllegalStateException(String.format(
- "Inconsistency between manifest.json and package.json, expected=%s, actual=%s", expected,
- srcEntry));
- }
- dest.copyFrom(srcPackagePath, srcEntry);
- }
-
- private static void copyAll(JsPluginManifestPath srcManifestPath, JsPluginsZipFilesystem dest) throws IOException {
- final JsPluginManifest manifestInfo = manifest(srcManifestPath);
- for (JsPluginManifestEntry manifestEntry : manifestInfo.plugins()) {
- final JsPluginPackagePath packagePath = srcManifestPath.packagePath(manifestEntry.name());
- copy(packagePath, dest, manifestEntry);
- }
- }
-
- private static JsPluginManifest manifest(JsPluginManifestPath manifest) throws IOException {
- // jackson impl does buffering internally
- try (final InputStream in = Files.newInputStream(manifest.manifestJson())) {
- return OBJECT_MAPPER.readValue(in, JsPluginManifest.class);
- }
- }
-
- private static JsPluginManifestEntry entry(JsPluginPackagePath packagePath) throws IOException {
- // jackson impl does buffering internally
- try (final InputStream in = Files.newInputStream(packagePath.packageJson())) {
- return OBJECT_MAPPER.readValue(in, JsPluginManifestEntry.class);
- }
}
}
diff --git a/server/jetty/src/main/java/io/deephaven/server/jetty/JsPluginsZipFilesystem.java b/server/jetty/src/main/java/io/deephaven/server/jetty/JsPluginsZipFilesystem.java
index 554ac81098b..7250a8e71b0 100644
--- a/server/jetty/src/main/java/io/deephaven/server/jetty/JsPluginsZipFilesystem.java
+++ b/server/jetty/src/main/java/io/deephaven/server/jetty/JsPluginsZipFilesystem.java
@@ -4,8 +4,8 @@
package io.deephaven.server.jetty;
import io.deephaven.configuration.CacheDir;
-import io.deephaven.plugin.js.JsPluginManifestPath;
-import io.deephaven.plugin.js.JsPluginPackagePath;
+import io.deephaven.plugin.js.JsPlugin;
+import io.deephaven.server.plugin.js.JsPluginManifest;
import java.io.IOException;
import java.io.OutputStream;
@@ -14,6 +14,7 @@
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.PathMatcher;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
@@ -21,6 +22,7 @@
import java.util.Objects;
import static io.deephaven.server.jetty.Json.OBJECT_MAPPER;
+import static io.deephaven.server.plugin.js.JsPluginManifest.MANIFEST_JSON;
class JsPluginsZipFilesystem {
private static final String ZIP_ROOT = "/";
@@ -44,37 +46,38 @@ public static JsPluginsZipFilesystem create() throws IOException {
}
private final URI filesystem;
- private final List entries;
+ private final List plugins;
private JsPluginsZipFilesystem(URI filesystem) {
this.filesystem = Objects.requireNonNull(filesystem);
- this.entries = new ArrayList<>();
+ this.plugins = new ArrayList<>();
}
public URI filesystem() {
return filesystem;
}
- public synchronized void copyFrom(JsPluginPackagePath srcPackagePath, JsPluginManifestEntry srcEntry)
- throws IOException {
- checkExisting(srcEntry);
+ public synchronized void add(JsPlugin plugin) throws IOException {
+ checkExisting(plugin.name());
// TODO(deephaven-core#3005): js-plugins checksum-based caching
// Note: FileSystem#close is necessary to write out contents for ZipFileSystem
try (final FileSystem fs = FileSystems.newFileSystem(filesystem, Map.of())) {
- final JsPluginManifestPath manifest = manifest(fs);
- copyRecursive(srcPackagePath, manifest.packagePath(srcEntry.name()));
- entries.add(srcEntry);
+ final Path manifestRoot = manifestRoot(fs);
+ final Path dstPath = manifestRoot.resolve(plugin.name());
+ // This is using internal knowledge that paths() must be PathsInternal and extends PathsMatcher.
+ final PathMatcher pathMatcher = (PathMatcher) plugin.paths();
+ // If listing and traversing the contents of development directories (and skipping the copy) becomes
+ // too expensive, we can add logic here wrt PathsInternal/PathsPrefix to specify a dirMatcher. Or,
+ // properly route directly from the filesystem via Jetty.
+ CopyHelper.copyRecursive(plugin.path(), dstPath, pathMatcher);
+ plugins.add(plugin);
writeManifest(fs);
}
}
- private static void copyRecursive(JsPluginPackagePath src, JsPluginPackagePath dst) throws IOException {
- CopyHelper.copyRecursive(src.path(), dst.path());
- }
-
- private void checkExisting(JsPluginManifestEntry info) {
- for (JsPluginManifestEntry existing : entries) {
- if (info.name().equals(existing.name())) {
+ private void checkExisting(String name) {
+ for (JsPlugin existing : plugins) {
+ if (name.equals(existing.name())) {
// TODO(deephaven-core#3048): Improve JS plugin support around plugins with conflicting names
throw new IllegalArgumentException(String.format(
"js plugin with name '%s' already exists. See https://github.com/deephaven/deephaven-core/issues/3048",
@@ -91,11 +94,11 @@ private synchronized void init() throws IOException {
}
private void writeManifest(FileSystem fs) throws IOException {
- final Path manifestJson = manifest(fs).manifestJson();
+ final Path manifestJson = manifestRoot(fs).resolve(MANIFEST_JSON);
final Path manifestJsonTmp = manifestJson.resolveSibling(manifestJson.getFileName().toString() + ".tmp");
// jackson impl does buffering internally
try (final OutputStream out = Files.newOutputStream(manifestJsonTmp)) {
- OBJECT_MAPPER.writeValue(out, JsPluginManifest.of(entries));
+ OBJECT_MAPPER.writeValue(out, manifest());
out.flush();
}
Files.move(manifestJsonTmp, manifestJson,
@@ -104,7 +107,11 @@ private void writeManifest(FileSystem fs) throws IOException {
StandardCopyOption.ATOMIC_MOVE);
}
- private static JsPluginManifestPath manifest(FileSystem fs) {
- return JsPluginManifestPath.of(fs.getPath(ZIP_ROOT));
+ private JsPluginManifest manifest() {
+ return JsPluginManifest.from(plugins);
+ }
+
+ private static Path manifestRoot(FileSystem fs) {
+ return fs.getPath(ZIP_ROOT);
}
}
diff --git a/server/jetty/src/test/java/io/deephaven/server/jetty/JettyFlightRoundTripTest.java b/server/jetty/src/test/java/io/deephaven/server/jetty/JettyFlightRoundTripTest.java
index 9d00dfd06b9..7278b18ee3f 100644
--- a/server/jetty/src/test/java/io/deephaven/server/jetty/JettyFlightRoundTripTest.java
+++ b/server/jetty/src/test/java/io/deephaven/server/jetty/JettyFlightRoundTripTest.java
@@ -6,20 +6,27 @@
import dagger.Component;
import dagger.Module;
import dagger.Provides;
-import io.deephaven.server.arrow.ArrowModule;
-import io.deephaven.server.config.ConfigServiceModule;
-import io.deephaven.server.console.ConsoleModule;
-import io.deephaven.server.log.LogModule;
+import io.deephaven.server.jetty.js.Example123Registration;
+import io.deephaven.server.jetty.js.Sentinel;
+import io.deephaven.server.plugin.js.JsPluginsManifestRegistration;
+import io.deephaven.server.plugin.js.JsPluginsNpmPackageRegistration;
import io.deephaven.server.runner.ExecutionContextUnitTestModule;
-import io.deephaven.server.session.ObfuscatingErrorTransformerModule;
-import io.deephaven.server.session.SessionModule;
-import io.deephaven.server.table.TableModule;
-import io.deephaven.server.test.TestAuthModule;
import io.deephaven.server.test.FlightMessageRoundTripTest;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.junit.Test;
import javax.inject.Singleton;
+import java.nio.file.Path;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import static org.assertj.core.api.Assertions.assertThat;
public class JettyFlightRoundTripTest extends FlightMessageRoundTripTest {
@@ -36,18 +43,10 @@ static JettyConfig providesJettyConfig() {
@Singleton
@Component(modules = {
- ArrowModule.class,
- ConfigServiceModule.class,
- ConsoleModule.class,
ExecutionContextUnitTestModule.class,
FlightTestModule.class,
JettyServerModule.class,
JettyTestConfig.class,
- LogModule.class,
- SessionModule.class,
- TableModule.class,
- TestAuthModule.class,
- ObfuscatingErrorTransformerModule.class,
})
public interface JettyTestComponent extends TestComponent {
}
@@ -56,4 +55,142 @@ public interface JettyTestComponent extends TestComponent {
protected TestComponent component() {
return DaggerJettyFlightRoundTripTest_JettyTestComponent.create();
}
+
+ @Test
+ public void jsPlugins() throws Exception {
+ // Note: JettyFlightRoundTripTest is not the most minimal / appropriate bootstrapping for this test, but it is
+ // the most convenient since it has all of the necessary prerequisites
+ new Example123Registration().registerInto(component.registration());
+ testJsPluginExamples(false, true, true);
+ }
+
+ @Test
+ public void jsPluginsFromManifest() throws Exception {
+ // Note: JettyFlightRoundTripTest is not the most minimal / appropriate bootstrapping for this test, but it is
+ // the most convenient since it has all of the necessary prerequisites
+ final Path manifestRoot = Path.of(Sentinel.class.getResource("examples").toURI());
+ new JsPluginsManifestRegistration(manifestRoot)
+ .registerInto(component.registration());
+ testJsPluginExamples(false, false, true);
+ }
+
+ @Test
+ public void jsPluginsFromNpmPackages() throws Exception {
+ // Note: JettyFlightRoundTripTest is not the most minimal / appropriate bootstrapping for this test, but it is
+ // the most convenient since it has all of the necessary prerequisites
+ final Path example1Root = Path.of(Sentinel.class.getResource("examples/@deephaven_test/example1").toURI());
+ final Path example2Root = Path.of(Sentinel.class.getResource("examples/@deephaven_test/example2").toURI());
+ // example3 is *not* a npm package, no package.json.
+ new JsPluginsNpmPackageRegistration(example1Root)
+ .registerInto(component.registration());
+ new JsPluginsNpmPackageRegistration(example2Root)
+ .registerInto(component.registration());
+ testJsPluginExamples(true, true, false);
+ }
+
+ private void testJsPluginExamples(boolean example1IsLimited, boolean example2IsLimited, boolean hasExample3)
+ throws Exception {
+ final HttpClient client = new HttpClient();
+ client.start();
+ try {
+ if (hasExample3) {
+ manifestTest123(client);
+ } else {
+ manifestTest12(client);
+ }
+ example1Tests(client, example1IsLimited);
+ example2Tests(client, example2IsLimited);
+ if (hasExample3) {
+ example3Tests(client);
+ }
+ } finally {
+ client.stop();
+ }
+ }
+
+ private void manifestTest12(HttpClient client) throws InterruptedException, TimeoutException, ExecutionException {
+ final ContentResponse manifestResponse = get(client, "js-plugins/manifest.json");
+ assertOk(manifestResponse, "application/json",
+ "{\"plugins\":[{\"name\":\"@deephaven_test/example1\",\"version\":\"0.1.0\",\"main\":\"dist/index.js\"},{\"name\":\"@deephaven_test/example2\",\"version\":\"0.2.0\",\"main\":\"dist/index.js\"}]}");
+ }
+
+ private void manifestTest123(HttpClient client) throws InterruptedException, TimeoutException, ExecutionException {
+ final ContentResponse manifestResponse = get(client, "js-plugins/manifest.json");
+ assertOk(manifestResponse, "application/json",
+ "{\"plugins\":[{\"name\":\"@deephaven_test/example1\",\"version\":\"0.1.0\",\"main\":\"dist/index.js\"},{\"name\":\"@deephaven_test/example2\",\"version\":\"0.2.0\",\"main\":\"dist/index.js\"},{\"name\":\"@deephaven_test/example3\",\"version\":\"0.3.0\",\"main\":\"index.js\"}]}");
+ }
+
+ private void example1Tests(HttpClient client, boolean isLimited)
+ throws InterruptedException, TimeoutException, ExecutionException {
+ if (isLimited) {
+ assertThat(get(client, "js-plugins/@deephaven_test/example1/package.json").getStatus())
+ .isEqualTo(HttpStatus.NOT_FOUND_404);
+ } else {
+ assertOk(get(client, "js-plugins/@deephaven_test/example1/package.json"),
+ "application/json",
+ "{\"name\":\"@deephaven_test/example1\",\"version\":\"0.1.0\",\"main\":\"dist/index.js\",\"files\":[\"dist\"]}");
+ }
+
+ assertOk(
+ get(client, "js-plugins/@deephaven_test/example1/dist/index.js"),
+ "text/javascript",
+ "// example1/dist/index.js");
+
+ assertOk(
+ get(client, "js-plugins/@deephaven_test/example1/dist/index2.js"),
+ "text/javascript",
+ "// example1/dist/index2.js");
+ }
+
+ private void example2Tests(HttpClient client, boolean isLimited)
+ throws InterruptedException, TimeoutException, ExecutionException {
+ if (isLimited) {
+ assertThat(get(client, "js-plugins/@deephaven_test/example2/package.json").getStatus())
+ .isEqualTo(HttpStatus.NOT_FOUND_404);
+ } else {
+ assertOk(get(client, "js-plugins/@deephaven_test/example2/package.json"),
+ "application/json",
+ "{\"name\":\"@deephaven_test/example2\",\"version\":\"0.2.0\",\"main\":\"dist/index.js\",\"files\":[\"dist\"]}");
+ }
+
+ assertOk(
+ get(client, "js-plugins/@deephaven_test/example2/dist/index.js"),
+ "text/javascript",
+ "// example2/dist/index.js");
+
+ assertOk(
+ get(client, "js-plugins/@deephaven_test/example2/dist/index2.js"),
+ "text/javascript",
+ "// example2/dist/index2.js");
+ }
+
+ private void example3Tests(HttpClient client) throws InterruptedException, TimeoutException, ExecutionException {
+ assertOk(
+ get(client, "js-plugins/@deephaven_test/example3/index.js"),
+ "text/javascript",
+ "// example3/index.js");
+ }
+
+ private ContentResponse get(HttpClient client, String path)
+ throws InterruptedException, TimeoutException, ExecutionException {
+ return client
+ .newRequest("localhost", localPort)
+ .path(path)
+ .method(HttpMethod.GET)
+ .send();
+ }
+
+ private static void assertOk(ContentResponse response, String contentType, String expected) {
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200);
+ assertThat(response.getMediaType()).isEqualTo(contentType);
+ assertThat(response.getContentAsString()).isEqualTo(expected);
+ assertNoCache(response);
+ }
+
+ private static void assertNoCache(ContentResponse response) {
+ final HttpFields headers = response.getHeaders();
+ assertThat(headers.getDateField("Expires")).isEqualTo(0);
+ assertThat(headers.get("Pragma")).isEqualTo("no-cache");
+ assertThat(headers.get("Cache-control")).isEqualTo("no-cache, must-revalidate, pre-check=0, post-check=0");
+ }
}
diff --git a/server/jetty/src/test/java/io/deephaven/server/jetty/js/Example123Registration.java b/server/jetty/src/test/java/io/deephaven/server/jetty/js/Example123Registration.java
new file mode 100644
index 00000000000..9fd0e7300fb
--- /dev/null
+++ b/server/jetty/src/test/java/io/deephaven/server/jetty/js/Example123Registration.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.server.jetty.js;
+
+import io.deephaven.plugin.Registration;
+import io.deephaven.plugin.js.JsPlugin;
+import io.deephaven.plugin.js.Paths;
+
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+
+public final class Example123Registration implements Registration {
+
+ public Example123Registration() {}
+
+ @Override
+ public void registerInto(Callback callback) {
+ final JsPlugin example1;
+ final JsPlugin example2;
+ final JsPlugin example3;
+ try {
+ example1 = example1();
+ example2 = example2();
+ example3 = example3();
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ callback.register(example1);
+ callback.register(example2);
+ callback.register(example3);
+ }
+
+ private static JsPlugin example1() throws URISyntaxException {
+ final Path resourcePath = Path.of(Sentinel.class.getResource("examples/@deephaven_test/example1").toURI());
+ final Path main = resourcePath.relativize(resourcePath.resolve("dist/index.js"));
+ return JsPlugin.builder()
+ .name("@deephaven_test/example1")
+ .version("0.1.0")
+ .main(main)
+ .path(resourcePath)
+ .build();
+ }
+
+ private static JsPlugin example2() throws URISyntaxException {
+ final Path resourcePath = Path.of(Sentinel.class.getResource("examples/@deephaven_test/example2").toURI());
+ final Path dist = resourcePath.relativize(resourcePath.resolve("dist"));
+ final Path main = dist.resolve("index.js");
+ return JsPlugin.builder()
+ .name("@deephaven_test/example2")
+ .version("0.2.0")
+ .main(main)
+ .path(resourcePath)
+ .paths(Paths.ofPrefixes(dist))
+ .build();
+ }
+
+ private static JsPlugin example3() throws URISyntaxException {
+ final Path resourcePath = Path.of(Sentinel.class.getResource("examples/@deephaven_test/example3").toURI());
+ final Path main = resourcePath.relativize(resourcePath.resolve("index.js"));
+ return JsPlugin.builder()
+ .name("@deephaven_test/example3")
+ .version("0.3.0")
+ .main(main)
+ .path(resourcePath)
+ .build();
+ }
+}
diff --git a/server/jetty/src/test/java/io/deephaven/server/jetty/js/Sentinel.java b/server/jetty/src/test/java/io/deephaven/server/jetty/js/Sentinel.java
new file mode 100644
index 00000000000..ff61539b94e
--- /dev/null
+++ b/server/jetty/src/test/java/io/deephaven/server/jetty/js/Sentinel.java
@@ -0,0 +1,8 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.server.jetty.js;
+
+public class Sentinel {
+ // just for the class
+}
diff --git a/server/jetty/src/test/java/io/deephaven/server/plugin/js/JsPluginsManifestRegistration.java b/server/jetty/src/test/java/io/deephaven/server/plugin/js/JsPluginsManifestRegistration.java
new file mode 100644
index 00000000000..508eae4521e
--- /dev/null
+++ b/server/jetty/src/test/java/io/deephaven/server/plugin/js/JsPluginsManifestRegistration.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.server.plugin.js;
+
+import io.deephaven.plugin.Registration;
+import io.deephaven.plugin.js.JsPlugin;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Objects;
+
+public class JsPluginsManifestRegistration implements Registration {
+
+ private final Path path;
+
+ public JsPluginsManifestRegistration(Path path) {
+ this.path = Objects.requireNonNull(path);
+ }
+
+ @Override
+ public void registerInto(Callback callback) {
+ final List plugins;
+ try {
+ plugins = JsPluginsFromManifest.of(path);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ for (JsPlugin plugin : plugins) {
+ callback.register(plugin);
+ }
+ }
+}
diff --git a/server/jetty/src/test/java/io/deephaven/server/plugin/js/JsPluginsNpmPackageRegistration.java b/server/jetty/src/test/java/io/deephaven/server/plugin/js/JsPluginsNpmPackageRegistration.java
new file mode 100644
index 00000000000..ded46d30b7d
--- /dev/null
+++ b/server/jetty/src/test/java/io/deephaven/server/plugin/js/JsPluginsNpmPackageRegistration.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.server.plugin.js;
+
+import io.deephaven.plugin.Registration;
+import io.deephaven.plugin.js.JsPlugin;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.util.Objects;
+
+public class JsPluginsNpmPackageRegistration implements Registration {
+
+ private final Path path;
+
+ public JsPluginsNpmPackageRegistration(Path path) {
+ this.path = Objects.requireNonNull(path);
+ }
+
+ @Override
+ public void registerInto(Callback callback) {
+ final JsPlugin plugin;
+ try {
+ plugin = JsPluginFromNpmPackage.of(path);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ callback.register(plugin);
+ }
+}
diff --git a/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example1/dist/index.js b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example1/dist/index.js
new file mode 100644
index 00000000000..de46952cc24
--- /dev/null
+++ b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example1/dist/index.js
@@ -0,0 +1 @@
+// example1/dist/index.js
\ No newline at end of file
diff --git a/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example1/dist/index2.js b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example1/dist/index2.js
new file mode 100644
index 00000000000..ef31df67846
--- /dev/null
+++ b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example1/dist/index2.js
@@ -0,0 +1 @@
+// example1/dist/index2.js
\ No newline at end of file
diff --git a/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example1/package.json b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example1/package.json
new file mode 100644
index 00000000000..b3733e4fe6d
--- /dev/null
+++ b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example1/package.json
@@ -0,0 +1 @@
+{"name":"@deephaven_test/example1","version":"0.1.0","main":"dist/index.js","files":["dist"]}
\ No newline at end of file
diff --git a/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example2/dist/index.js b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example2/dist/index.js
new file mode 100644
index 00000000000..f84594080ee
--- /dev/null
+++ b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example2/dist/index.js
@@ -0,0 +1 @@
+// example2/dist/index.js
\ No newline at end of file
diff --git a/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example2/dist/index2.js b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example2/dist/index2.js
new file mode 100644
index 00000000000..536a8edceee
--- /dev/null
+++ b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example2/dist/index2.js
@@ -0,0 +1 @@
+// example2/dist/index2.js
\ No newline at end of file
diff --git a/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example2/package.json b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example2/package.json
new file mode 100644
index 00000000000..64ca82a446b
--- /dev/null
+++ b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example2/package.json
@@ -0,0 +1 @@
+{"name":"@deephaven_test/example2","version":"0.2.0","main":"dist/index.js","files":["dist"]}
\ No newline at end of file
diff --git a/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example3/index.js b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example3/index.js
new file mode 100644
index 00000000000..62c773b6ad4
--- /dev/null
+++ b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example3/index.js
@@ -0,0 +1 @@
+// example3/index.js
\ No newline at end of file
diff --git a/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/manifest.json b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/manifest.json
new file mode 100644
index 00000000000..eaa745d1b6b
--- /dev/null
+++ b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/manifest.json
@@ -0,0 +1,19 @@
+{
+ "plugins": [
+ {
+ "name": "@deephaven_test/example1",
+ "main": "dist/index.js",
+ "version": "0.1.0"
+ },
+ {
+ "name": "@deephaven_test/example2",
+ "main": "dist/index.js",
+ "version": "0.2.0"
+ },
+ {
+ "name": "@deephaven_test/example3",
+ "main": "index.js",
+ "version": "0.3.0"
+ }
+ ]
+}
diff --git a/server/netty/src/test/java/io/deephaven/server/netty/NettyFlightRoundTripTest.java b/server/netty/src/test/java/io/deephaven/server/netty/NettyFlightRoundTripTest.java
index 1876b0924c2..89cc9833d26 100644
--- a/server/netty/src/test/java/io/deephaven/server/netty/NettyFlightRoundTripTest.java
+++ b/server/netty/src/test/java/io/deephaven/server/netty/NettyFlightRoundTripTest.java
@@ -6,15 +6,7 @@
import dagger.Component;
import dagger.Module;
import dagger.Provides;
-import io.deephaven.server.arrow.ArrowModule;
-import io.deephaven.server.config.ConfigServiceModule;
-import io.deephaven.server.console.ConsoleModule;
-import io.deephaven.server.log.LogModule;
import io.deephaven.server.runner.ExecutionContextUnitTestModule;
-import io.deephaven.server.session.ObfuscatingErrorTransformerModule;
-import io.deephaven.server.session.SessionModule;
-import io.deephaven.server.table.TableModule;
-import io.deephaven.server.test.TestAuthModule;
import io.deephaven.server.test.FlightMessageRoundTripTest;
import javax.inject.Singleton;
@@ -36,18 +28,10 @@ static NettyConfig providesNettyConfig() {
@Singleton
@Component(modules = {
- ArrowModule.class,
- ConfigServiceModule.class,
- ConsoleModule.class,
ExecutionContextUnitTestModule.class,
FlightTestModule.class,
- LogModule.class,
NettyServerModule.class,
NettyTestConfig.class,
- SessionModule.class,
- TableModule.class,
- TestAuthModule.class,
- ObfuscatingErrorTransformerModule.class,
})
public interface NettyTestComponent extends TestComponent {
}
diff --git a/server/src/main/java/io/deephaven/server/plugin/PluginRegistrationVisitor.java b/server/src/main/java/io/deephaven/server/plugin/PluginRegistrationVisitor.java
index 79d766f66f8..97ec345a555 100644
--- a/server/src/main/java/io/deephaven/server/plugin/PluginRegistrationVisitor.java
+++ b/server/src/main/java/io/deephaven/server/plugin/PluginRegistrationVisitor.java
@@ -11,7 +11,6 @@
import javax.inject.Inject;
import java.util.Objects;
-import java.util.function.Consumer;
/**
* Plugin {@link io.deephaven.plugin.Registration.Callback} implementation that forwards registered plugins to a
diff --git a/server/src/main/java/io/deephaven/server/plugin/js/Jackson.java b/server/src/main/java/io/deephaven/server/plugin/js/Jackson.java
new file mode 100644
index 00000000000..1454b3e5735
--- /dev/null
+++ b/server/src/main/java/io/deephaven/server/plugin/js/Jackson.java
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.server.plugin.js;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+class Jackson {
+ static final ObjectMapper OBJECT_MAPPER =
+ new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+}
diff --git a/server/src/main/java/io/deephaven/server/plugin/js/JsPluginConfigDirRegistration.java b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginConfigDirRegistration.java
new file mode 100644
index 00000000000..96b36d7d157
--- /dev/null
+++ b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginConfigDirRegistration.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.server.plugin.js;
+
+import dagger.Binds;
+import dagger.multibindings.IntoSet;
+import io.deephaven.configuration.ConfigDir;
+import io.deephaven.plugin.Registration;
+import io.deephaven.plugin.js.JsPlugin;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import static io.deephaven.server.plugin.js.JsPluginManifest.MANIFEST_JSON;
+
+
+/**
+ * Registers the {@link JsPlugin JS plugins} sourced from the {@link JsPluginManifest manifest} root located at
+ * {@link ConfigDir} / {@value JS_PLUGINS} (if {@value io.deephaven.server.plugin.js.JsPluginManifest#MANIFEST_JSON}
+ * exists).
+ */
+public final class JsPluginConfigDirRegistration implements Registration {
+
+ public static final String JS_PLUGINS = "js-plugins";
+
+ /**
+ * Binds {@link JsPluginConfigDirRegistration} into the set of {@link Registration}.
+ */
+ @dagger.Module
+ public interface Module {
+ @Binds
+ @IntoSet
+ Registration bindsRegistration(JsPluginConfigDirRegistration registration);
+ }
+
+ @Inject
+ JsPluginConfigDirRegistration() {}
+
+ @Override
+ public void registerInto(Callback callback) {
+ // /js-plugins/ (manifest root)
+ final Path manifestRoot = ConfigDir.get()
+ .map(p -> p.resolve(JS_PLUGINS).resolve(MANIFEST_JSON))
+ .filter(Files::exists)
+ .map(Path::getParent)
+ .orElse(null);
+ if (manifestRoot == null) {
+ return;
+ }
+ final List plugins;
+ try {
+ plugins = JsPluginsFromManifest.of(manifestRoot);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ for (JsPlugin plugin : plugins) {
+ callback.register(plugin);
+ }
+ }
+}
diff --git a/server/src/main/java/io/deephaven/server/plugin/js/JsPluginFromNpmPackage.java b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginFromNpmPackage.java
new file mode 100644
index 00000000000..a966904ba62
--- /dev/null
+++ b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginFromNpmPackage.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.server.plugin.js;
+
+import io.deephaven.plugin.js.JsPlugin;
+import io.deephaven.plugin.js.JsPlugin.Builder;
+import io.deephaven.plugin.js.Paths;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+class JsPluginFromNpmPackage {
+
+ static JsPlugin of(Path packageRoot) throws IOException {
+ final Path packageJsonPath = packageRoot.resolve(JsPluginNpmPackageRegistration.PACKAGE_JSON);
+ final NpmPackage packageJson = NpmPackage.read(packageJsonPath);
+ final Path main = packageRoot.relativize(packageRoot.resolve(packageJson.main()));
+ final Paths paths;
+ if (main.getNameCount() > 1) {
+ // We're requiring that all of the necessary files to serve be under the top-level directory as sourced from
+ // package.json/main. For example, "build/index.js" -> "build", "dist/bundle/index.js" -> "dist". This
+ // supports development use cases where the top-level directory may be interspersed with unrelated
+ // development files (node_modules, .git, etc).
+ //
+ // Note: this logic only comes into play for development use cases where plugins are configured via
+ // deephaven.jsPlugins.myPlugin=/path/to/my/js
+ paths = Paths.ofPrefixes(main.subpath(0, 1));
+ } else {
+ paths = Paths.all();
+ }
+ final Builder builder = JsPlugin.builder()
+ .name(packageJson.name())
+ .version(packageJson.version())
+ .main(main)
+ .path(packageRoot)
+ .paths(paths);
+ return builder.build();
+ }
+}
diff --git a/server/src/main/java/io/deephaven/server/plugin/js/JsPluginManifest.java b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginManifest.java
new file mode 100644
index 00000000000..a74bb7e1c77
--- /dev/null
+++ b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginManifest.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.server.plugin.js;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.deephaven.annotations.SimpleStyle;
+import io.deephaven.plugin.js.JsPlugin;
+import org.immutables.value.Value.Immutable;
+import org.immutables.value.Value.Parameter;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static io.deephaven.server.plugin.js.Jackson.OBJECT_MAPPER;
+
+@Immutable
+@SimpleStyle
+public abstract class JsPluginManifest {
+ public static final String PLUGINS = "plugins";
+ public static final String MANIFEST_JSON = "manifest.json";
+
+ @JsonCreator
+ public static JsPluginManifest of(
+ @JsonProperty(value = PLUGINS, required = true) List plugins) {
+ return ImmutableJsPluginManifest.of(plugins);
+ }
+
+ public static JsPluginManifest from(List plugins) {
+ return of(plugins.stream().map(JsPluginManifestEntry::from).collect(Collectors.toList()));
+ }
+
+ static JsPluginManifest read(Path manifestJson) throws IOException {
+ // jackson impl does buffering internally
+ try (final InputStream in = Files.newInputStream(manifestJson)) {
+ return OBJECT_MAPPER.readValue(in, JsPluginManifest.class);
+ }
+ }
+
+ @Parameter
+ @JsonProperty(PLUGINS)
+ public abstract List plugins();
+}
diff --git a/server/jetty/src/main/java/io/deephaven/server/jetty/JsPluginManifestEntry.java b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginManifestEntry.java
similarity index 82%
rename from server/jetty/src/main/java/io/deephaven/server/jetty/JsPluginManifestEntry.java
rename to server/src/main/java/io/deephaven/server/plugin/js/JsPluginManifestEntry.java
index 385397809d9..4aeca8b05ab 100644
--- a/server/jetty/src/main/java/io/deephaven/server/jetty/JsPluginManifestEntry.java
+++ b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginManifestEntry.java
@@ -1,11 +1,12 @@
/**
* Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
*/
-package io.deephaven.server.jetty;
+package io.deephaven.server.plugin.js;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.deephaven.annotations.SimpleStyle;
+import io.deephaven.plugin.js.JsPlugin;
import org.immutables.value.Value.Immutable;
import org.immutables.value.Value.Parameter;
@@ -14,7 +15,7 @@
*/
@Immutable
@SimpleStyle
-abstract class JsPluginManifestEntry {
+public abstract class JsPluginManifestEntry {
public static final String NAME = "name";
public static final String VERSION = "version";
@@ -28,6 +29,10 @@ public static JsPluginManifestEntry of(
return ImmutableJsPluginManifestEntry.of(name, version, main);
}
+ public static JsPluginManifestEntry from(JsPlugin plugin) {
+ return of(plugin.name(), plugin.version(), plugin.main().toString());
+ }
+
/**
* The name of the plugin.
*/
diff --git a/server/src/main/java/io/deephaven/server/plugin/js/JsPluginManifestRegistration.java b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginManifestRegistration.java
new file mode 100644
index 00000000000..93f1aa66f39
--- /dev/null
+++ b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginManifestRegistration.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.server.plugin.js;
+
+import dagger.Binds;
+import dagger.multibindings.IntoSet;
+import io.deephaven.configuration.Configuration;
+import io.deephaven.plugin.Registration;
+import io.deephaven.plugin.js.JsPlugin;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Registers the {@link JsPlugin JS plugins} sourced from the {@link JsPluginManifest manifest} root configuration
+ * property {@value JsPluginManifestRegistration#JS_PLUGIN_RESOURCE_BASE}.
+ */
+public final class JsPluginManifestRegistration implements Registration {
+
+ public static final String JS_PLUGIN_RESOURCE_BASE = JsPluginModule.DEEPHAVEN_JS_PLUGINS_PREFIX + "resourceBase";
+
+ /**
+ * Binds {@link JsPluginManifestRegistration} into the set of {@link Registration}.
+ */
+ @dagger.Module
+ public interface Module {
+ @Binds
+ @IntoSet
+ Registration bindsRegistration(JsPluginManifestRegistration registration);
+ }
+
+ @Inject
+ JsPluginManifestRegistration() {}
+
+ @Override
+ public void registerInto(Callback callback) {
+ // deephaven.jsPlugins.resourceBase (manifest root)
+ final String resourceBase = Configuration.getInstance().getStringWithDefault(JS_PLUGIN_RESOURCE_BASE, null);
+ if (resourceBase == null) {
+ return;
+ }
+ final List plugins;
+ try {
+ plugins = JsPluginsFromManifest.of(Path.of(resourceBase));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ for (JsPlugin plugin : plugins) {
+ callback.register(plugin);
+ }
+ }
+}
diff --git a/server/src/main/java/io/deephaven/server/plugin/js/JsPluginModule.java b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginModule.java
index 06f0dcf4ea9..1b966150836 100644
--- a/server/src/main/java/io/deephaven/server/plugin/js/JsPluginModule.java
+++ b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginModule.java
@@ -4,116 +4,19 @@
package io.deephaven.server.plugin.js;
import dagger.Module;
-import dagger.Provides;
-import dagger.multibindings.ElementsIntoSet;
import io.deephaven.configuration.ConfigDir;
-import io.deephaven.configuration.Configuration;
-import io.deephaven.plugin.Registration;
-import io.deephaven.plugin.js.JsPluginManifestPath;
-import io.deephaven.plugin.js.JsPluginPackagePath;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Optional;
-import java.util.Set;
/**
- * Provides the {@link JsPluginManifestPath manifest path} of {@value JS_PLUGIN_RESOURCE_BASE} if the configuration
- * property is set. Provides the {@link JsPluginManifestPath manifest path} of {@link ConfigDir} / {@value JS_PLUGINS}
- * if {@value JsPluginManifestPath#MANIFEST_JSON} exists. Provides the {@link JsPluginPackagePath package path} for all
- * configuration properties that start with {@value DEEPHAVEN_JS_PLUGINS_PREFIX} and have a single part after.
+ * Includes the modules {@link JsPluginManifestRegistration.Module}, {@link JsPluginConfigDirRegistration.Module}, and
+ * {@link JsPluginNpmPackageRegistration.Module}; these modules add various means of configuration support for producing
+ * and registering {@link io.deephaven.plugin.js.JsPlugin}.
*/
-@Module
+@Module(includes = {
+ JsPluginManifestRegistration.Module.class,
+ JsPluginConfigDirRegistration.Module.class,
+ JsPluginNpmPackageRegistration.Module.class,
+})
public interface JsPluginModule {
String DEEPHAVEN_JS_PLUGINS_PREFIX = "deephaven.jsPlugins.";
- String JS_PLUGIN_RESOURCE_BASE = DEEPHAVEN_JS_PLUGINS_PREFIX + "resourceBase";
- String JS_PLUGINS = "js-plugins";
-
- @Provides
- @ElementsIntoSet
- static Set providesResourceBaseRegistration() {
- return jsPluginsResourceBase()
- .map(Registration.class::cast)
- .map(Set::of)
- .orElseGet(Set::of);
- }
-
- @Provides
- @ElementsIntoSet
- static Set providesConfigDirRegistration() {
- return jsPluginsConfigDir()
- .map(Registration.class::cast)
- .map(Set::of)
- .orElseGet(Set::of);
- }
-
- @Provides
- @ElementsIntoSet
- static Set providesPackageRoots() {
- return Set.copyOf(jsPluginsPackageRoots());
- }
-
- // deephaven.jsPlugins.resourceBase (manifest root)
- private static Optional jsPluginsResourceBase() {
- final String resourceBase = Configuration.getInstance().getStringWithDefault(JS_PLUGIN_RESOURCE_BASE, null);
- return Optional.ofNullable(resourceBase)
- .map(Path::of)
- .map(JsPluginManifestPath::of);
- }
-
- // /js-plugins/ (manifest root)
- private static Optional jsPluginsConfigDir() {
- return ConfigDir.get()
- .map(JsPluginModule::resolveJsPlugins)
- .map(JsPluginManifestPath::of)
- .filter(JsPluginModule::manifestJsonExists);
- }
-
- private static Path resolveJsPlugins(Path p) {
- return p.resolve(JS_PLUGINS);
- }
-
- private static boolean manifestJsonExists(JsPluginManifestPath path) {
- return Files.exists(path.manifestJson());
- }
-
- // deephaven.jsPlugins. (package root)
- private static Set jsPluginsPackageRoots() {
- final Configuration config = Configuration.getInstance();
- final Set parts = partsThatStartWith(DEEPHAVEN_JS_PLUGINS_PREFIX, config);
- final Set packageRoots = new HashSet<>(parts.size());
- for (String part : parts) {
- final String propertyName = DEEPHAVEN_JS_PLUGINS_PREFIX + part;
- if (JS_PLUGIN_RESOURCE_BASE.equals(propertyName)) {
- // handled by jsPluginsResourceBase
- continue;
- }
- final String packageRoot = config.getStringWithDefault(propertyName, null);
- if (packageRoot == null) {
- continue;
- }
- packageRoots.add(JsPluginPackagePath.of(Path.of(packageRoot)));
- }
- return packageRoots;
- }
-
- private static Set partsThatStartWith(String prefix, Configuration configuration) {
- final Set parts = new HashSet<>();
- final Iterator