diff --git a/polyglot/src/main/java/org/restheart/polyglot/AbstractJSPlugin.java b/polyglot/src/main/java/org/restheart/polyglot/AbstractJSPlugin.java deleted file mode 100644 index 48cd499cec..0000000000 --- a/polyglot/src/main/java/org/restheart/polyglot/AbstractJSPlugin.java +++ /dev/null @@ -1,165 +0,0 @@ -/*- - * ========================LICENSE_START================================= - * restheart-polyglot - * %% - * Copyright (C) 2020 - 2024 SoftInstigate - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * =========================LICENSE_END================================== - */ -package org.restheart.polyglot; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import com.mongodb.client.MongoClient; - -import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.Engine; -import org.graalvm.polyglot.HostAccess; -import org.graalvm.polyglot.Source; -import org.graalvm.polyglot.io.IOAccess; -import org.restheart.configuration.Configuration; -import org.restheart.plugins.InterceptPoint; -import org.restheart.plugins.RegisterPlugin.MATCH_POLICY; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public abstract class AbstractJSPlugin { - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractJSPlugin.class); - - protected Map contextOptions = new HashMap<>(); - - protected Engine engine = Engine.create(); - - protected String modulesReplacements; - protected Source handleSource; - - protected String name; - protected String pluginClass; - protected String description; - protected String uri; - protected boolean secured; - protected MATCH_POLICY matchPolicy; - protected InterceptPoint interceptPoint; - - protected boolean isService; - protected boolean isInterceptor; - - protected Optional mclient; - protected Configuration conf; - - protected AbstractJSPlugin() { - this.name = null; - this.pluginClass = null; - this.description = null; - this.uri = null; - this.secured = false; - this.matchPolicy = null; - this.interceptPoint = null; - this.conf = null; - this.isService = true; - this.isInterceptor = false; - } - - protected AbstractJSPlugin(String name, - String pluginClass, - String description, - String uri, - boolean secured, - MATCH_POLICY matchPolicy, - InterceptPoint interceptPoint, - Configuration conf, - boolean isService, - boolean isInterceptor) { - this.name = name; - this.pluginClass = pluginClass; - this.description = description; - this.uri = uri; - this.secured = secured; - this.matchPolicy = matchPolicy; - this.interceptPoint = interceptPoint; - this.conf = conf; - this.isService = isService; - this.isInterceptor = isInterceptor; - } - - public static Context context(Engine engine, Map OPTS) { - return Context.newBuilder().engine(engine) - .allowAllAccess(true) - .allowHostAccess(HostAccess.ALL) - .allowHostClassLookup(className -> true) - .allowIO(IOAccess.ALL) - .allowExperimentalOptions(true) - .options(OPTS) - .build(); - } - - public String getName() { - return name; - } - - public String getPluginClass() { - return pluginClass; - } - - public String getUri() { - return uri; - } - - public String getDescription() { - return description; - } - - public boolean isSecured() { - return secured; - } - - public MATCH_POLICY getMatchPolicy() { - return matchPolicy; - } - - public InterceptPoint getInterceptPoint() { - return interceptPoint; - } - - public static void addBindings(Context ctx, - String pluginName, - Configuration conf, - Logger LOGGER, - Optional mclient) { - ctx.getBindings("js").putMember("LOGGER", LOGGER); - - if (mclient.isPresent()) { - ctx.getBindings("js").putMember("mclient", mclient.get()); - } - - var args = conf != null - ? conf.getOrDefault(pluginName, new HashMap()) - : new HashMap(); - - ctx.getBindings("js").putMember("pluginArgs", args); - } - - /** - * - * @return the Context - */ - protected Context ctx() { - var ret = context(engine, contextOptions); - addBindings(ret, pluginClass, conf, LOGGER, mclient); - return ret; - } -} diff --git a/polyglot/src/main/java/org/restheart/polyglot/ContextQueue.java b/polyglot/src/main/java/org/restheart/polyglot/ContextQueue.java new file mode 100644 index 0000000000..4fd4e1a37a --- /dev/null +++ b/polyglot/src/main/java/org/restheart/polyglot/ContextQueue.java @@ -0,0 +1,103 @@ +/*- + * ========================LICENSE_START================================= + * restheart-polyglot + * %% + * Copyright (C) 2020 - 2024 SoftInstigate + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * =========================LICENSE_END================================== + */ +package org.restheart.polyglot; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ArrayBlockingQueue; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Engine; +import org.graalvm.polyglot.HostAccess; +import org.graalvm.polyglot.io.IOAccess; +import org.restheart.configuration.Configuration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mongodb.client.MongoClient; + +public class ContextQueue { + private static final Logger LOGGER = LoggerFactory.getLogger(ContextQueue.class); + private final int QUEUE_SIZE = 4 * Runtime.getRuntime().availableProcessors(); + private final ArrayBlockingQueue QUEUE = new ArrayBlockingQueue<>(QUEUE_SIZE); + + public ContextQueue(Engine engine, String name, Configuration conf, Logger logger, Optional mclient, String modulesReplacements, Map OPTS) { + for (var c = 0; c < QUEUE_SIZE; c++) { + QUEUE.add(newContext(engine, name, conf, logger, mclient, modulesReplacements, OPTS)); + } + } + + public Context take() throws InterruptedException { + return QUEUE.take(); + } + + public void release(Context ctx) { + try { + QUEUE.add(ctx); + } catch(IllegalStateException ise) { + try (ctx) { + // queue is full + LOGGER.warn("Error releasing Context in {}", Thread.currentThread().getName()); + } + } + } + + public static Context newContext(Engine engine, String name, Configuration conf, Logger LOGGER, Optional mclient, String modulesReplacements, Map OPTS) { + if (modulesReplacements!= null) { + LOGGER.debug("modules-replacements: {} ", modulesReplacements); + OPTS.put("js.commonjs-core-modules-replacements", modulesReplacements); + } else { + OPTS.remove("js.commonjs-core-modules-replacements"); + } + + var ctx = Context.newBuilder().engine(engine) + .allowAllAccess(true) + .allowHostAccess(HostAccess.ALL) + .allowHostClassLookup(className -> true) + .allowIO(IOAccess.ALL) + .allowExperimentalOptions(true) + .options(OPTS) + .build(); + + addBindings(ctx, name, conf, LOGGER, mclient); + + return ctx; + } + + private static void addBindings(Context ctx, + String pluginName, + Configuration conf, + Logger logger, + Optional mclient) { + ctx.getBindings("js").putMember("LOGGER", logger); + + if (mclient.isPresent()) { + ctx.getBindings("js").putMember("mclient", mclient.get()); + } + + var args = conf != null + ? conf.getOrDefault(pluginName, new HashMap<>()) + : new HashMap(); + + ctx.getBindings("js").putMember("pluginArgs", args); + } +} \ No newline at end of file diff --git a/polyglot/src/main/java/org/restheart/polyglot/JSPlugin.java b/polyglot/src/main/java/org/restheart/polyglot/JSPlugin.java new file mode 100644 index 0000000000..adc87f1337 --- /dev/null +++ b/polyglot/src/main/java/org/restheart/polyglot/JSPlugin.java @@ -0,0 +1,131 @@ +/*- + * ========================LICENSE_START================================= + * restheart-polyglot + * %% + * Copyright (C) 2020 - 2024 SoftInstigate + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * =========================LICENSE_END================================== + */ +package org.restheart.polyglot; + +import java.util.Map; +import java.util.Optional; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Engine; +import org.graalvm.polyglot.Source; +import org.restheart.configuration.Configuration; +import org.restheart.polyglot.services.JSServiceArgs; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mongodb.client.MongoClient; + +public abstract class JSPlugin { + protected static final Logger LOGGER = LoggerFactory.getLogger(JSPlugin.class); + + private static final Engine engine = Engine.create(); + + private final String modulesReplacements; + private final Source handleSource; + + private final String name; + private final String description; + private final Optional mclient; + private final Configuration configuration; + + protected ContextQueue contextQueue; + + /** + * + * @param name + * @param configuration + * @param description + * @param modulesReplacements + * @param handleSource + * @param mclient + * @param opts + */ + public JSPlugin(String name, + String description, + Source handleSource, + String modulesReplacements, + Configuration configuration, + Optional mclient, + Map opts) { + this.name = name; + this.description = description; + this.handleSource = handleSource; + this.mclient = mclient; + this.configuration = configuration; + this.modulesReplacements = modulesReplacements; + this.contextQueue = new ContextQueue(engine, name, configuration, LOGGER, mclient, modulesReplacements, opts); + } + + /** + * + * @param args + */ + public JSPlugin(JSServiceArgs args) { + this.name = args.name(); + this.description = args.description(); + this.handleSource = args.handleSource(); + this.mclient = args.mclient(); + this.configuration = args.configuration(); + this.modulesReplacements = args.modulesReplacements(); + this.contextQueue = new ContextQueue(engine, name, configuration, LOGGER, mclient, modulesReplacements, args.contextOptions()); + } + + public String name() { + return name; + } + + public String getDescription() { + return description; + } + + /** + * + * @return the Context + * @throws java.lang.InterruptedException + */ + protected Context takeCtx() throws InterruptedException { + return this.contextQueue.take(); + } + + /** + * + * @param ctx + */ + protected void releaseCtx(Context ctx) { + this.contextQueue.release(ctx); + } + + public Optional mclient() { + return mclient; + } + + public Source handleSource() { + return handleSource; + } + + public Configuration configuration() { + return configuration; + } + + public static Engine engine() { + return engine; + } +} diff --git a/polyglot/src/main/java/org/restheart/polyglot/PolyglotDeployer.java b/polyglot/src/main/java/org/restheart/polyglot/PolyglotDeployer.java index c89a88d152..bfdf2dc9ee 100644 --- a/polyglot/src/main/java/org/restheart/polyglot/PolyglotDeployer.java +++ b/polyglot/src/main/java/org/restheart/polyglot/PolyglotDeployer.java @@ -63,6 +63,11 @@ import org.restheart.exchange.ServiceRequest; import org.restheart.exchange.ServiceResponse; import org.restheart.graal.ImageInfo; +import org.restheart.polyglot.interceptors.JSInterceptor; +import org.restheart.polyglot.interceptors.JSInterceptorFactory; +import org.restheart.polyglot.services.JSService; +import org.restheart.polyglot.services.JSStringService; +import org.restheart.polyglot.services.NodeService; import org.restheart.utils.ThreadsUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -80,7 +85,7 @@ public class PolyglotDeployer implements Initializer { private Path pluginsDirectory = null; - private static final Map DEPLOYEES = new HashMap<>(); + private static final Map DEPLOYEES = new HashMap<>(); private JSInterceptorFactory jsInterceptorFactory; @@ -344,7 +349,7 @@ private List findDeclaredPlugins(Path path, String prop, boolean checkPlug } } - private void deploy(List services, List nodeServices, List interceptors) throws IOException { + private void deploy(List services, List nodeServices, List interceptors) throws IOException, InterruptedException { for (Path service: services) { deployService(service); } @@ -358,28 +363,28 @@ private void deploy(List services, List nodeServices, List int } } - private void deployService(Path pluginPath) throws IOException { + private void deployService(Path pluginPath) throws IOException, InterruptedException { if (isRunningOnNode()) { throw new IllegalStateException("Cannot deploy a CommonJs service, RESTHeart is running on Node"); } try { - var srv = new JavaScriptService(pluginPath, this.mclient, this.config); + var srv = new JSStringService(pluginPath, this.mclient, this.config); - var record = new PluginRecord, ? extends ServiceResponse>>(srv.getName(), + var record = new PluginRecord, ? extends ServiceResponse>>(srv.name(), srv.getDescription(), - srv.isSecured(), + srv.secured(), true, srv.getClass().getName(), srv, new HashMap<>()); - registry.plugService(record, srv.getUri(), srv.getMatchPolicy(), srv.isSecured()); + registry.plugService(record, srv.uri(), srv.matchPolicy(), srv.secured()); DEPLOYEES.put(pluginPath.toAbsolutePath(), srv); LOGGER.info(ansi().fg(GREEN).a("URI {} bound to service {}, description: {}, secured: {}, uri match {}").reset().toString(), - srv.getUri(), srv.getName(), srv.getDescription(), srv.isSecured(), srv.getMatchPolicy()); + srv.uri(), srv.name(), srv.getDescription(), srv.secured(), srv.matchPolicy()); } catch(IllegalArgumentException | IllegalStateException e) { LOGGER.error("Error deploying plugin {}", pluginPath, e); } @@ -402,15 +407,15 @@ private void deployNodeService(Path pluginPath) throws IOException { var srv = srvf.get(); - var record = new PluginRecord, ? extends ServiceResponse>>(srv.getName(), "description", srv.isSecured(), true, + var record = new PluginRecord, ? extends ServiceResponse>>(srv.name(), "description", srv.secured(), true, srv.getClass().getName(), srv, new HashMap<>()); - registry.plugService(record, srv.getUri(), srv.getMatchPolicy(), srv.isSecured()); + registry.plugService(record, srv.uri(), srv.matchPolicy(), srv.secured()); DEPLOYEES.put(pluginPath.toAbsolutePath(), srv); LOGGER.info(ansi().fg(GREEN).a("URI {} bound to service {}, description: {}, secured: {}, uri match {}").reset().toString(), - srv.getUri(), srv.getName(), srv.getDescription(), srv.isSecured(), srv.getMatchPolicy()); + srv.uri(), srv.name(), srv.getDescription(), srv.secured(), srv.matchPolicy()); } catch (IOException | InterruptedException | ExecutionException ex) { LOGGER.error("Error deploying node service {}", pluginPath, ex); Thread.currentThread().interrupt(); @@ -421,7 +426,7 @@ var record = new PluginRecord, ? extends Ser } - private void deployInterceptor(Path pluginPath) throws IOException { + private void deployInterceptor(Path pluginPath) throws IOException, InterruptedException { if (isRunningOnNode()) { throw new IllegalStateException("Cannot deploy a CommonJs interceptor, RESTHeart is running on Node"); } @@ -430,7 +435,7 @@ private void deployInterceptor(Path pluginPath) throws IOException { registry.addInterceptor(interceptorRecord); - DEPLOYEES.put(pluginPath.toAbsolutePath(), (AbstractJSPlugin) interceptorRecord.getInstance()); + DEPLOYEES.put(pluginPath.toAbsolutePath(), (JSPlugin) interceptorRecord.getInstance()); LOGGER.info(ansi().fg(GREEN).a("Added interceptor {}, description: {}").reset().toString(), interceptorRecord.getName(), @@ -444,36 +449,35 @@ private void undeploy(Path pluginPath) { private void undeployServices(Path pluginPath) { var pathsToUndeploy = DEPLOYEES.keySet().stream() - .filter(path -> DEPLOYEES.get(path).isService) + .filter(path -> (DEPLOYEES.get(path) instanceof JSService)) .filter(path -> path.startsWith(pluginPath)) .collect(Collectors.toList()); for (var pathToUndeploy: pathsToUndeploy) { - var toUndeploy = DEPLOYEES.remove(pathToUndeploy); + var _toUndeploy = DEPLOYEES.remove(pathToUndeploy); - if (toUndeploy != null) { - registry.unplug(toUndeploy.getUri(), toUndeploy.getMatchPolicy()); + if (_toUndeploy != null && _toUndeploy instanceof JSService toUndeploy) { + registry.unplug(toUndeploy.uri(), toUndeploy.matchPolicy()); - LOGGER.info(ansi().fg(GREEN).a("removed service {} bound to URI {}").reset().toString(), - toUndeploy.getName(), toUndeploy.getUri()); + LOGGER.info(ansi().fg(GREEN).a("removed service {} bound to URI {}").reset().toString(), toUndeploy.name(), toUndeploy.uri()); } } } private void undeployInterceptors(Path pluginPath) { var pathsToUndeploy = DEPLOYEES.keySet().stream() - .filter(path -> DEPLOYEES.get(path).isInterceptor) + .filter(path -> DEPLOYEES.get(path) instanceof JSInterceptor) .filter(path -> path.startsWith(pluginPath)) .collect(Collectors.toList()); for (var pathToUndeploy: pathsToUndeploy) { var toUndeploy = DEPLOYEES.remove(pathToUndeploy); - var removed = registry.removeInterceptorIf(interceptor -> Objects.equal(interceptor.getName(), toUndeploy.getName())); + var removed = registry.removeInterceptorIf(interceptor -> Objects.equal(interceptor.getName(), toUndeploy.name())); if (removed) { - LOGGER.info(ansi().fg(GREEN).a("removed interceptor {}").reset().toString(), toUndeploy.getName()); + LOGGER.info(ansi().fg(GREEN).a("removed interceptor {}").reset().toString(), toUndeploy.name()); } else { - LOGGER.warn("interceptor {} was not removed", toUndeploy.getName()); + LOGGER.warn("interceptor {} was not removed", toUndeploy.name()); } } } diff --git a/polyglot/src/main/java/org/restheart/polyglot/interceptors/BsonJSInterceptor.java b/polyglot/src/main/java/org/restheart/polyglot/interceptors/BsonJSInterceptor.java index d164db752c..b8c7ca11da 100644 --- a/polyglot/src/main/java/org/restheart/polyglot/interceptors/BsonJSInterceptor.java +++ b/polyglot/src/main/java/org/restheart/polyglot/interceptors/BsonJSInterceptor.java @@ -23,24 +23,25 @@ import java.util.Map; import java.util.Optional; -import com.mongodb.client.MongoClient; - import org.graalvm.polyglot.Source; import org.restheart.configuration.Configuration; import org.restheart.exchange.BsonRequest; import org.restheart.exchange.BsonResponse; import org.restheart.plugins.InterceptPoint; -public class BsonJSInterceptor extends AbstractJSInterceptor { +import com.mongodb.client.MongoClient; + +public class BsonJSInterceptor extends JSInterceptor { public BsonJSInterceptor(String name, String pluginClass, String description, InterceptPoint interceptPoint, + String modulesReplacements, Source handleSource, Source resolveSource, Optional mclient, Configuration config, Map contextOptions) { - super(name, pluginClass, description, interceptPoint, handleSource, resolveSource, mclient, config, contextOptions); + super(name, pluginClass, description, interceptPoint, modulesReplacements, handleSource, resolveSource, mclient, config, contextOptions); } } \ No newline at end of file diff --git a/polyglot/src/main/java/org/restheart/polyglot/interceptors/ByteArrayJSInterceptor.java b/polyglot/src/main/java/org/restheart/polyglot/interceptors/ByteArrayJSInterceptor.java index 758791da14..548e348751 100644 --- a/polyglot/src/main/java/org/restheart/polyglot/interceptors/ByteArrayJSInterceptor.java +++ b/polyglot/src/main/java/org/restheart/polyglot/interceptors/ByteArrayJSInterceptor.java @@ -23,24 +23,25 @@ import java.util.Map; import java.util.Optional; -import com.mongodb.client.MongoClient; - import org.graalvm.polyglot.Source; import org.restheart.configuration.Configuration; import org.restheart.exchange.ByteArrayRequest; import org.restheart.exchange.ByteArrayResponse; import org.restheart.plugins.InterceptPoint; -public class ByteArrayJSInterceptor extends AbstractJSInterceptor { +import com.mongodb.client.MongoClient; + +public class ByteArrayJSInterceptor extends JSInterceptor { public ByteArrayJSInterceptor(String name, String pluginClass, String description, InterceptPoint interceptPoint, + String modulesReplacements, Source handleSource, Source resolveSource, Optional mclient, Configuration config, Map contextOptions) { - super(name, pluginClass, description, interceptPoint, handleSource, resolveSource, mclient, config, contextOptions); + super(name, pluginClass, description, interceptPoint, modulesReplacements, handleSource, resolveSource, mclient, config, contextOptions); } } \ No newline at end of file diff --git a/polyglot/src/main/java/org/restheart/polyglot/interceptors/ByteArrayProxyJSInterceptor.java b/polyglot/src/main/java/org/restheart/polyglot/interceptors/ByteArrayProxyJSInterceptor.java index 2bc8223ae3..3b265f6644 100644 --- a/polyglot/src/main/java/org/restheart/polyglot/interceptors/ByteArrayProxyJSInterceptor.java +++ b/polyglot/src/main/java/org/restheart/polyglot/interceptors/ByteArrayProxyJSInterceptor.java @@ -23,24 +23,25 @@ import java.util.Map; import java.util.Optional; -import com.mongodb.client.MongoClient; - import org.graalvm.polyglot.Source; import org.restheart.configuration.Configuration; import org.restheart.exchange.ByteArrayProxyRequest; import org.restheart.exchange.ByteArrayProxyResponse; import org.restheart.plugins.InterceptPoint; -public class ByteArrayProxyJSInterceptor extends AbstractJSInterceptor { +import com.mongodb.client.MongoClient; + +public class ByteArrayProxyJSInterceptor extends JSInterceptor { public ByteArrayProxyJSInterceptor(String name, String pluginClass, String description, InterceptPoint interceptPoint, + String modulesReplacements, Source handleSource, Source resolveSource, Optional mclient, Configuration config, Map contextOptions) { - super(name, pluginClass, description, interceptPoint, handleSource, resolveSource, mclient, config, contextOptions); + super(name, pluginClass, description, interceptPoint, modulesReplacements, handleSource, resolveSource, mclient, config, contextOptions); } } \ No newline at end of file diff --git a/polyglot/src/main/java/org/restheart/polyglot/interceptors/CsvJSInterceptor.java b/polyglot/src/main/java/org/restheart/polyglot/interceptors/CsvJSInterceptor.java index b1d4d7d624..5768414886 100644 --- a/polyglot/src/main/java/org/restheart/polyglot/interceptors/CsvJSInterceptor.java +++ b/polyglot/src/main/java/org/restheart/polyglot/interceptors/CsvJSInterceptor.java @@ -23,24 +23,25 @@ import java.util.Map; import java.util.Optional; -import com.mongodb.client.MongoClient; - import org.graalvm.polyglot.Source; import org.restheart.configuration.Configuration; import org.restheart.exchange.BsonFromCsvRequest; import org.restheart.exchange.BsonResponse; import org.restheart.plugins.InterceptPoint; -public class CsvJSInterceptor extends AbstractJSInterceptor { +import com.mongodb.client.MongoClient; + +public class CsvJSInterceptor extends JSInterceptor { public CsvJSInterceptor(String name, String pluginClass, String description, InterceptPoint interceptPoint, + String modulesReplacements, Source handleSource, Source resolveSource, Optional mclient, Configuration config, Map contextOptions) { - super(name, pluginClass, description, interceptPoint, handleSource, resolveSource, mclient, config, contextOptions); + super(name, pluginClass, description, interceptPoint, modulesReplacements, handleSource, resolveSource, mclient, config, contextOptions); } } \ No newline at end of file diff --git a/polyglot/src/main/java/org/restheart/polyglot/interceptors/AbstractJSInterceptor.java b/polyglot/src/main/java/org/restheart/polyglot/interceptors/JSInterceptor.java similarity index 55% rename from polyglot/src/main/java/org/restheart/polyglot/interceptors/AbstractJSInterceptor.java rename to polyglot/src/main/java/org/restheart/polyglot/interceptors/JSInterceptor.java index 43593dfa79..9f4b7f1a3c 100644 --- a/polyglot/src/main/java/org/restheart/polyglot/interceptors/AbstractJSInterceptor.java +++ b/polyglot/src/main/java/org/restheart/polyglot/interceptors/JSInterceptor.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Optional; +import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Source; import org.graalvm.polyglot.Value; import org.restheart.configuration.Configuration; @@ -30,16 +31,13 @@ import org.restheart.exchange.Response; import org.restheart.plugins.InterceptPoint; import org.restheart.plugins.Interceptor; -import org.restheart.polyglot.AbstractJSPlugin; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.restheart.polyglot.JSPlugin; -import com.google.common.collect.Maps; import com.mongodb.client.MongoClient; -public class AbstractJSInterceptor, S extends Response> extends AbstractJSPlugin implements Interceptor { - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractJSInterceptor.class); - +public class JSInterceptor, S extends Response> extends JSPlugin implements Interceptor { + private final String pluginClass; + private final InterceptPoint interceptPoint; private final Source resolveSource; /** @@ -47,69 +45,80 @@ public class AbstractJSInterceptor, S extends Response> * @param pluginClass * @param description * @param interceptPoint + * @param modulesReplacements * @param handleSource * @param mclient * @param resolveSource * @param contextOptions * @param config */ - public AbstractJSInterceptor(String name, + public JSInterceptor(String name, String pluginClass, String description, InterceptPoint interceptPoint, + String modulesReplacements, Source handleSource, Source resolveSource, Optional mclient, Configuration config, Map contextOptions) { - super(name, pluginClass, description, null, false, null, interceptPoint, config, false, true); - this.contextOptions = contextOptions; - this.mclient = mclient; - this.conf = config; - this.handleSource = handleSource; + super(name, description, handleSource, modulesReplacements, config, mclient, contextOptions); + this.pluginClass = pluginClass; + this.interceptPoint = interceptPoint; this.resolveSource = resolveSource; } + public InterceptPoint getInterceptPoint() { + return interceptPoint; + } + /** * * @param request - * @param response */ + * @param response + * @throws java.lang.InterruptedException */ @Override - public void handle(R request, S response) { - try (final var ctx = ctx()) { - ctx.eval(this.handleSource).executeVoid(request, response); + public void handle(R request, S response) throws InterruptedException { + Context ctx = null; + + try { + ctx = takeCtx(); + ctx.eval(handleSource()).executeVoid(request, response); + } finally { + if (ctx != null) { + releaseCtx(ctx); + } } } @Override public boolean resolve(R request, S response) { - var ret = resolve().execute(request); + Context ctx = null; + Value ret = null; + + try { + ctx = takeCtx(); + ret = ctx.eval(this.resolveSource).execute(request); + } catch(InterruptedException ie) { + LOGGER.error("error on interceptor {} resolve()", name(), ie); + request.setInError(true); + return false; + } finally { + if (ctx != null) { + releaseCtx(ctx); + } + } - if (ret.isBoolean()) { + if (ret != null && ret.isBoolean()) { return ret.asBoolean(); } else { - LOGGER.error("resolve() of interceptor did not returned a boolean", name); + LOGGER.error("resolve() of interceptor {} did not returned a boolean", name()); + request.setInError(true); return false; } } - // each working thread is associates with one resolve - // because js Context does not allow multithreaded access - protected Map resolves = Maps.newHashMap(); - - /** - * - * @return the resolve Value associated with this thread. If not existing, it instanitates it. - */ - private Value resolve() { - var workingThreadName = Thread.currentThread().getName(); - - if (this.resolves.containsKey(workingThreadName)) { - return this.resolves.get(workingThreadName); - } else { - var resolve = ctx().eval(this.resolveSource); - this.resolves.put(workingThreadName, resolve); - return resolve; - } + public String getPluginClass() { + return pluginClass; } } diff --git a/polyglot/src/main/java/org/restheart/polyglot/JSInterceptorFactory.java b/polyglot/src/main/java/org/restheart/polyglot/interceptors/JSInterceptorFactory.java similarity index 87% rename from polyglot/src/main/java/org/restheart/polyglot/JSInterceptorFactory.java rename to polyglot/src/main/java/org/restheart/polyglot/interceptors/JSInterceptorFactory.java index 17edee03e0..2d43bf0206 100644 --- a/polyglot/src/main/java/org/restheart/polyglot/JSInterceptorFactory.java +++ b/polyglot/src/main/java/org/restheart/polyglot/interceptors/JSInterceptorFactory.java @@ -18,7 +18,7 @@ * along with this program. If not, see . * =========================LICENSE_END================================== */ -package org.restheart.polyglot; +package org.restheart.polyglot.interceptors; import java.io.IOException; import java.nio.file.Files; @@ -27,6 +27,7 @@ import java.util.Map; import java.util.Optional; +import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Engine; import org.graalvm.polyglot.Source; import org.graalvm.polyglot.Value; @@ -36,14 +37,8 @@ import org.restheart.plugins.InterceptPoint; import org.restheart.plugins.Interceptor; import org.restheart.plugins.PluginRecord; -import static org.restheart.polyglot.AbstractJSPlugin.addBindings; -import org.restheart.polyglot.interceptors.AbstractJSInterceptor; -import org.restheart.polyglot.interceptors.ByteArrayJSInterceptor; -import org.restheart.polyglot.interceptors.ByteArrayProxyJSInterceptor; -import org.restheart.polyglot.interceptors.CsvJSInterceptor; -import org.restheart.polyglot.interceptors.JsonJSInterceptor; -import org.restheart.polyglot.interceptors.MongoJSInterceptor; -import org.restheart.polyglot.interceptors.StringJSInterceptor; +import org.restheart.polyglot.ContextQueue; +import org.restheart.polyglot.JSPlugin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,7 +50,7 @@ * @author Andrea Di Cesare {@literal } */ public class JSInterceptorFactory { - private static final Logger LOGGER = LoggerFactory.getLogger(JSInterceptorFactory.class); + private static final Logger LOGGER = LoggerFactory.getLogger(JSPlugin.class); Map contextOptions = new HashMap<>(); @@ -70,7 +65,7 @@ public JSInterceptorFactory(Optional mclient, Configuration config) this.config = config; } - public PluginRecord> create(Path pluginPath) throws IOException { + public PluginRecord> create(Path pluginPath) throws IOException, InterruptedException { // find plugin root, i.e the parent dir that contains package.json var pluginRoot = pluginPath.getParent(); while(true) { @@ -100,9 +95,8 @@ public JSInterceptorFactory(Optional mclient, Configuration config) // check plugin definition var sindexPath = pluginPath.toAbsolutePath().toString(); - try (var ctx = AbstractJSPlugin.context(engine, contextOptions)) { - // add bindings to contenxt - addBindings(ctx, "foo", null, LOGGER, this.mclient); + + try (Context ctx = ContextQueue.newContext(engine, "foo", config, LOGGER, mclient, "", contextOptions)) { // ******** evaluate and check options @@ -150,9 +144,9 @@ public JSInterceptorFactory(Optional mclient, Configuration config) var sb = new StringBuilder(); options.getMember("modulesReplacements").getMemberKeys().stream() - .forEach(k -> sb.append(k).append(":") - .append(options.getMember("modulesReplacements").getMember(k)) - .append(",")); + .forEach(k -> sb.append(k).append(":") + .append(options.getMember("modulesReplacements").getMember(k)) + .append(",")); modulesReplacements = sb.toString(); } @@ -181,8 +175,7 @@ public JSInterceptorFactory(Optional mclient, Configuration config) if (!options.getMemberKeys().contains("pluginClass")) { pluginClass = "StringInterceptor"; } else if (!options.getMember("pluginClass").isString()) { - throw new IllegalArgumentException( - "wrong js interceptor " + pluginPath.toAbsolutePath() + ", wrong member 'options.pluginClass', " + HANDLE_RESOLVE_HINT); + throw new IllegalArgumentException("wrong js interceptor " + pluginPath.toAbsolutePath() + ", wrong member 'options.pluginClass', " + HANDLE_RESOLVE_HINT); } else { pluginClass = options.getMember("pluginClass").asString(); } @@ -221,18 +214,16 @@ public JSInterceptorFactory(Optional mclient, Configuration config) throw new IllegalArgumentException("wrong js interceptor " + pluginPath.toAbsolutePath() + ", " + HANDLE_RESOLVE_HINT); } - AbstractJSInterceptor, ? extends Response> interceptor; - - + JSInterceptor, ? extends Response> interceptor; - Map opts = Maps.newHashMap(); - opts.putAll(contextOptions); + Map contextOpts = Maps.newHashMap(); + contextOpts.putAll(contextOptions); if (modulesReplacements != null) { LOGGER.debug("modules-replacements: {} ", modulesReplacements); - opts.put("js.commonjs-core-modules-replacements", modulesReplacements); + contextOpts.put("js.commonjs-core-modules-replacements", modulesReplacements); } else { - opts.remove("js.commonjs-core-modules-replacements"); + contextOpts.remove("js.commonjs-core-modules-replacements"); } switch (pluginClass) { @@ -240,69 +231,76 @@ public JSInterceptorFactory(Optional mclient, Configuration config) pluginClass, description, interceptPoint, + modulesReplacements, handleSource, resolveSource, mclient, config, - opts); + contextOpts); case "BsonInterceptor", "org.restheart.plugins.BsonInterceptor" -> interceptor = new StringJSInterceptor(name, pluginClass, description, interceptPoint, + modulesReplacements, handleSource, resolveSource, mclient, config, - opts); + contextOpts); case "ByteArrayInterceptor", "org.restheart.plugins.ByteArrayInterceptor" -> interceptor = new ByteArrayJSInterceptor(name, pluginClass, description, interceptPoint, + modulesReplacements, handleSource, resolveSource, mclient, config, - opts); + contextOpts); case "ByteArrayProxyInterceptor", "org.restheart.plugins.ByteArrayProxyInterceptor" -> interceptor = new ByteArrayProxyJSInterceptor(name, pluginClass, description, interceptPoint, + modulesReplacements, handleSource, resolveSource, mclient, config, - opts); + contextOpts); case "CsvInterceptor", "org.restheart.plugins.CsvInterceptor" -> interceptor = new CsvJSInterceptor(name, pluginClass, description, interceptPoint, + modulesReplacements, handleSource, resolveSource, mclient, config, - opts); + contextOpts); case "JsonInterceptor", "org.restheart.plugins.JsonInterceptor" -> interceptor = new JsonJSInterceptor(name, pluginClass, description, interceptPoint, + modulesReplacements, handleSource, resolveSource, mclient, config, - opts); + contextOpts); case "MongoInterceptor", "org.restheart.plugins.MongoInterceptor" -> interceptor = new MongoJSInterceptor(name, pluginClass, description, interceptPoint, + modulesReplacements, handleSource, resolveSource, mclient, config, - opts); + contextOpts); default -> throw new IllegalArgumentException("wrong js interceptor, wrong member 'options.pluginClass', " + PACKAGE_HINT); } - return new PluginRecord<>(interceptor.getName(), + return new PluginRecord<>(interceptor.name(), interceptor.getDescription(), false, true, diff --git a/polyglot/src/main/java/org/restheart/polyglot/interceptors/JsonJSInterceptor.java b/polyglot/src/main/java/org/restheart/polyglot/interceptors/JsonJSInterceptor.java index a52f70ee22..517f7d9322 100644 --- a/polyglot/src/main/java/org/restheart/polyglot/interceptors/JsonJSInterceptor.java +++ b/polyglot/src/main/java/org/restheart/polyglot/interceptors/JsonJSInterceptor.java @@ -23,24 +23,25 @@ import java.util.Map; import java.util.Optional; -import com.mongodb.client.MongoClient; - import org.graalvm.polyglot.Source; import org.restheart.configuration.Configuration; import org.restheart.exchange.JsonRequest; import org.restheart.exchange.JsonResponse; import org.restheart.plugins.InterceptPoint; -public class JsonJSInterceptor extends AbstractJSInterceptor { +import com.mongodb.client.MongoClient; + +public class JsonJSInterceptor extends JSInterceptor { public JsonJSInterceptor(String name, String pluginClass, String description, InterceptPoint interceptPoint, + String modulesReplacements, Source handleSource, Source resolveSource, Optional mclient, Configuration config, Map contextOptions) { - super(name, pluginClass, description, interceptPoint, handleSource, resolveSource, mclient, config, contextOptions); + super(name, pluginClass, description, interceptPoint, modulesReplacements, handleSource, resolveSource, mclient, config, contextOptions); } } \ No newline at end of file diff --git a/polyglot/src/main/java/org/restheart/polyglot/interceptors/JsonProxyJSInterceptor.java b/polyglot/src/main/java/org/restheart/polyglot/interceptors/JsonProxyJSInterceptor.java index 1af9495764..9975db9dc7 100644 --- a/polyglot/src/main/java/org/restheart/polyglot/interceptors/JsonProxyJSInterceptor.java +++ b/polyglot/src/main/java/org/restheart/polyglot/interceptors/JsonProxyJSInterceptor.java @@ -23,24 +23,25 @@ import java.util.Map; import java.util.Optional; -import com.mongodb.client.MongoClient; - import org.graalvm.polyglot.Source; import org.restheart.configuration.Configuration; import org.restheart.exchange.JsonProxyRequest; import org.restheart.exchange.JsonProxyResponse; import org.restheart.plugins.InterceptPoint; -public class JsonProxyJSInterceptor extends AbstractJSInterceptor { +import com.mongodb.client.MongoClient; + +public class JsonProxyJSInterceptor extends JSInterceptor { public JsonProxyJSInterceptor(String name, String pluginClass, String description, InterceptPoint interceptPoint, + String modulesReplacements, Source handleSource, Source resolveSource, Optional mclient, Configuration config, Map contextOptions) { - super(name, pluginClass, description, interceptPoint, handleSource, resolveSource, mclient, config, contextOptions); + super(name, pluginClass, description, interceptPoint, modulesReplacements, handleSource, resolveSource, mclient, config, contextOptions); } } \ No newline at end of file diff --git a/polyglot/src/main/java/org/restheart/polyglot/interceptors/MongoJSInterceptor.java b/polyglot/src/main/java/org/restheart/polyglot/interceptors/MongoJSInterceptor.java index b9783c9175..25d843ae51 100644 --- a/polyglot/src/main/java/org/restheart/polyglot/interceptors/MongoJSInterceptor.java +++ b/polyglot/src/main/java/org/restheart/polyglot/interceptors/MongoJSInterceptor.java @@ -23,24 +23,25 @@ import java.util.Map; import java.util.Optional; -import com.mongodb.client.MongoClient; - import org.graalvm.polyglot.Source; import org.restheart.configuration.Configuration; import org.restheart.exchange.MongoRequest; import org.restheart.exchange.MongoResponse; import org.restheart.plugins.InterceptPoint; -public class MongoJSInterceptor extends AbstractJSInterceptor { +import com.mongodb.client.MongoClient; + +public class MongoJSInterceptor extends JSInterceptor { public MongoJSInterceptor(String name, String pluginClass, String description, InterceptPoint interceptPoint, + String modulesReplacements, Source handleSource, Source resolveSource, Optional mclient, Configuration config, Map contextOptions) { - super(name, pluginClass, description, interceptPoint, handleSource, resolveSource, mclient, config, contextOptions); + super(name, pluginClass, description, interceptPoint, modulesReplacements, handleSource, resolveSource, mclient, config, contextOptions); } } \ No newline at end of file diff --git a/polyglot/src/main/java/org/restheart/polyglot/interceptors/StringJSInterceptor.java b/polyglot/src/main/java/org/restheart/polyglot/interceptors/StringJSInterceptor.java index e31df9d9f3..b748ea1aca 100644 --- a/polyglot/src/main/java/org/restheart/polyglot/interceptors/StringJSInterceptor.java +++ b/polyglot/src/main/java/org/restheart/polyglot/interceptors/StringJSInterceptor.java @@ -23,24 +23,25 @@ import java.util.Map; import java.util.Optional; -import com.mongodb.client.MongoClient; - import org.graalvm.polyglot.Source; import org.restheart.configuration.Configuration; import org.restheart.exchange.StringRequest; import org.restheart.exchange.StringResponse; import org.restheart.plugins.InterceptPoint; -public class StringJSInterceptor extends AbstractJSInterceptor { +import com.mongodb.client.MongoClient; + +public class StringJSInterceptor extends JSInterceptor { public StringJSInterceptor(String name, String pluginClass, String description, InterceptPoint interceptPoint, + String modulesReplacements, Source handleSource, Source resolveSource, Optional mclient, Configuration config, Map contextOptions) { - super(name, pluginClass, description, interceptPoint, handleSource, resolveSource, mclient, config, contextOptions); + super(name, pluginClass, description, interceptPoint, modulesReplacements, handleSource, resolveSource, mclient, config, contextOptions); } } \ No newline at end of file diff --git a/polyglot/src/main/java/org/restheart/polyglot/services/JSService.java b/polyglot/src/main/java/org/restheart/polyglot/services/JSService.java new file mode 100644 index 0000000000..f6835e63e7 --- /dev/null +++ b/polyglot/src/main/java/org/restheart/polyglot/services/JSService.java @@ -0,0 +1,81 @@ +/*- + * ========================LICENSE_START================================= + * restheart-security + * %% + * Copyright (C) 2018 - 2024 SoftInstigate + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * =========================LICENSE_END================================== + */ + +package org.restheart.polyglot.services; + +import java.util.Map; +import java.util.Optional; + +import org.graalvm.polyglot.Source; +import org.restheart.configuration.Configuration; +import org.restheart.plugins.RegisterPlugin.MATCH_POLICY; +import org.restheart.polyglot.JSPlugin; + +import com.mongodb.client.MongoClient; + + +/** + * + * @author Andrea Di Cesare {@literal } + */ +public abstract class JSService extends JSPlugin { + private final String uri; + private final boolean secured; + private final MATCH_POLICY matchPolicy; + + public JSService(String name, + String pluginClass, + String description, + String uri, + boolean secured, + MATCH_POLICY matchPolicy, + Source handleSource, + String modulesReplacements, + Configuration config, + Optional mclient, + Map contextOptions) { + super(name, description, handleSource, modulesReplacements, config, mclient, contextOptions); + this.uri = uri; + this.secured = secured; + this.matchPolicy = matchPolicy; + } + + + public JSService(JSServiceArgs args) { + super(args.name(), args.description(), args.handleSource(), args.modulesReplacements(), args.configuration(), args.mclient(), args.contextOptions()); + this.uri = args.uri(); + this.secured = args.secured(); + this.matchPolicy = args.matchPolicy(); + } + + public String uri() { + return uri; + } + + + public boolean secured() { + return secured; + } + + public MATCH_POLICY matchPolicy() { + return matchPolicy; + } +} \ No newline at end of file diff --git a/polyglot/src/main/java/org/restheart/polyglot/services/JSServiceArgs.java b/polyglot/src/main/java/org/restheart/polyglot/services/JSServiceArgs.java new file mode 100644 index 0000000000..e5ca090e3d --- /dev/null +++ b/polyglot/src/main/java/org/restheart/polyglot/services/JSServiceArgs.java @@ -0,0 +1,47 @@ +/*- + * ========================LICENSE_START================================= + * restheart-security + * %% + * Copyright (C) 2018 - 2024 SoftInstigate + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * =========================LICENSE_END================================== + */ + +package org.restheart.polyglot.services; + +import java.util.Map; +import java.util.Optional; + +import org.graalvm.polyglot.Source; +import org.restheart.configuration.Configuration; +import org.restheart.plugins.RegisterPlugin.MATCH_POLICY; + +import com.mongodb.client.MongoClient; + +/** + * + * @author Andrea Di Cesare {@literal } + */ +public record JSServiceArgs(String name, + String description, + String uri, + boolean secured, + String modulesReplacements, + MATCH_POLICY matchPolicy, + Source handleSource, + Configuration configuration, + Optional mclient, + Map contextOptions) { +} \ No newline at end of file diff --git a/polyglot/src/main/java/org/restheart/polyglot/JavaScriptService.java b/polyglot/src/main/java/org/restheart/polyglot/services/JSStringService.java similarity index 73% rename from polyglot/src/main/java/org/restheart/polyglot/JavaScriptService.java rename to polyglot/src/main/java/org/restheart/polyglot/services/JSStringService.java index 3d661503c3..456db677f4 100644 --- a/polyglot/src/main/java/org/restheart/polyglot/JavaScriptService.java +++ b/polyglot/src/main/java/org/restheart/polyglot/services/JSStringService.java @@ -18,11 +18,12 @@ * along with this program. If not, see . * =========================LICENSE_END================================== */ -package org.restheart.polyglot; +package org.restheart.polyglot.services; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; import java.util.Optional; import org.graalvm.polyglot.Context; @@ -33,8 +34,7 @@ import org.restheart.exchange.StringResponse; import org.restheart.plugins.RegisterPlugin.MATCH_POLICY; import org.restheart.plugins.StringService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.restheart.polyglot.ContextQueue; import com.mongodb.client.MongoClient; @@ -42,9 +42,7 @@ * * @author Andrea Di Cesare {@literal } */ -public class JavaScriptService extends AbstractJSPlugin implements StringService { - private static final Logger LOGGER = LoggerFactory.getLogger(JavaScriptService.class); - +public class JSStringService extends JSService implements StringService { private static final String HANDLE_HINT = """ the plugin module must export the function 'handle', example: export function handle(request, response) { @@ -71,13 +69,13 @@ export function handle(request, response) { } """; - JavaScriptService(Path pluginPath, Optional mclient, Configuration conf) throws IOException { - this.mclient = mclient; - this.conf = conf; - this.isService = true; - this.isInterceptor = false; + public JSStringService(Path pluginPath, Optional mclient, Configuration config) throws IOException, InterruptedException { + super(args(pluginPath, mclient, config)); + } + private static JSServiceArgs args(Path pluginPath, Optional mclient, Configuration config) throws IOException { // find plugin root, i.e the parent dir that contains package.json + var contextOptions = new HashMap(); var pluginRoot = pluginPath.getParent(); while(true) { var p = pluginRoot.resolve("package.json"); @@ -96,18 +94,15 @@ export function handle(request, response) { LOGGER.trace("Enabling require for service {} with require-cwd {} ", pluginPath, requireCwdPath); } - // check that the plugin script is js - var language = Source.findLanguage(pluginPath.toFile()); - - if (!"js".equals(language)) { - throw new IllegalArgumentException("wrong js plugin, not javascript"); - } + try (Context ctx = ContextQueue.newContext(engine(), "foo", config, LOGGER, mclient, "", contextOptions)) { + // check that the plugin script is js + var language = Source.findLanguage(pluginPath.toFile()); - var sindexPath = pluginPath.toAbsolutePath().toString(); - try (var ctx = context(engine, contextOptions)) { + if (!"js".equals(language)) { + throw new IllegalArgumentException("wrong js plugin, not javascript"); + } - // add bindings to contenxt - addBindings(ctx, this.name, conf, LOGGER, this.mclient); + var sindexPath = pluginPath.toAbsolutePath().toString(); var optionsScript = "import { options } from '" + sindexPath + "'; options;"; var optionsSource = Source.newBuilder(language, optionsScript, "optionsScript").mimeType("application/javascript+module").build(); @@ -128,84 +123,59 @@ export function handle(request, response) { checkOptions(options, pluginPath); - this.name = options.getMember("name").asString(); - this.description = options.getMember("description").asString(); - this.uri = options.getMember("uri").asString(); - - if (!options.getMemberKeys().contains("secured")) { - this.secured = false; - } else { - this.secured = options.getMember("secured").asBoolean(); - } - - if (!options.getMemberKeys().contains("matchPolicy")) { - this.matchPolicy = MATCH_POLICY.PREFIX; - } else { - var _matchPolicy = options.getMember("matchPolicy").asString(); - this.matchPolicy = MATCH_POLICY.valueOf(_matchPolicy); - } + var name = options.getMember("name").asString(); + var description = options.getMember("description").asString(); + var uri = options.getMember("uri").asString(); + var secured = !options.getMemberKeys().contains("secured") ? false : options.getMember("secured").asBoolean(); + var matchPolicy = !options.getMemberKeys().contains("matchPolicy") ? MATCH_POLICY.PREFIX : MATCH_POLICY.valueOf(options.getMember("matchPolicy").asString()); + String modulesReplacements = null; - if (!options.getMemberKeys().contains("modulesReplacements")) { - this.modulesReplacements = null; - } else { + if (options.getMemberKeys().contains("modulesReplacements")) { var sb = new StringBuilder(); options.getMember("modulesReplacements").getMemberKeys().stream() - .forEach(k -> sb.append(k).append(":") - .append(options.getMember("modulesReplacements").getMember(k)) - .append(",")); + .forEach(k -> sb.append(k).append(":") + .append(options.getMember("modulesReplacements").getMember(k)) + .append(",")); - this.modulesReplacements = sb.toString(); + modulesReplacements = sb.toString(); } // ******** evaluate and check handle - var handleScript = "import { handle } from '" + sindexPath + "'; handle;"; - this.handleSource = Source.newBuilder(language, handleScript, "handleScript").mimeType("application/javascript+module").build(); + var _handleScript = "import { handle } from '" + sindexPath + "'; handle;"; + var handleSource = Source.newBuilder(language, _handleScript, "handleScript").mimeType("application/javascript+module").build(); Value handle; try { - handle = ctx.eval(this.handleSource); + handle = ctx.eval(handleSource); } catch (Throwable t) { throw new IllegalArgumentException("wrong js service " + pluginPath.toAbsolutePath() + ", " + t.getMessage()); } checkHandle(handle, pluginPath); - } - } - /** - * - */ - @Override - public void handle(StringRequest request, StringResponse response) { - try (final var ctx = ctx()) { - ctx.eval(this.handleSource).executeVoid(request, response); + return new JSServiceArgs(name, description, uri, secured, modulesReplacements, matchPolicy, handleSource, config, mclient, contextOptions); } } + + /** * - * @return the Context - */ + * @throws java.lang.InterruptedException */ @Override - protected Context ctx() { - if (getModulesReplacements() != null) { - LOGGER.debug("modules-replacements: {} ", getModulesReplacements()); - contextOptions.put("js.commonjs-core-modules-replacements", getModulesReplacements()); - } else { - contextOptions.remove("js.commonjs-core-modules-replacements"); + public void handle(StringRequest request, StringResponse response) throws InterruptedException { + Context ctx = null; + try { + ctx = takeCtx(); + ctx.eval(handleSource()).executeVoid(request, response); + } finally { + if (ctx != null) { + releaseCtx(ctx); + } } - - var ctx = context(engine, contextOptions); - addBindings(ctx, this.name, conf, LOGGER, this.mclient); - - return ctx; - } - - public String getModulesReplacements() { - return this.modulesReplacements; } static void checkOptions(Value options, Path pluginPath) { @@ -272,4 +242,4 @@ static void checkHandle(Value handle, Path pluginPath) { throw new IllegalArgumentException("wrong js service " + pluginPath.toAbsolutePath() + ", " + HANDLE_HINT); } } -} +} \ No newline at end of file diff --git a/polyglot/src/main/java/org/restheart/polyglot/NodeService.java b/polyglot/src/main/java/org/restheart/polyglot/services/NodeService.java similarity index 75% rename from polyglot/src/main/java/org/restheart/polyglot/NodeService.java rename to polyglot/src/main/java/org/restheart/polyglot/services/NodeService.java index c34b78a0a4..ec3032f3a6 100644 --- a/polyglot/src/main/java/org/restheart/polyglot/NodeService.java +++ b/polyglot/src/main/java/org/restheart/polyglot/services/NodeService.java @@ -18,11 +18,12 @@ * along with this program. If not, see . * =========================LICENSE_END================================== */ -package org.restheart.polyglot; +package org.restheart.polyglot.services; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; import java.util.concurrent.LinkedBlockingDeque; import java.util.Optional; import java.util.concurrent.Executors; @@ -37,6 +38,7 @@ import org.restheart.exchange.StringResponse; import org.restheart.plugins.StringService; import org.restheart.plugins.RegisterPlugin.MATCH_POLICY; +import org.restheart.polyglot.NodeQueue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,14 +46,11 @@ * * @author Andrea Di Cesare {@literal } */ -public class NodeService extends AbstractJSPlugin implements StringService { - private static final Logger LOGGER = LoggerFactory.getLogger(NodeService.class); - +public class NodeService extends JSService implements StringService { private String source; - private int codeHash = 0; - private static final String errorHint = """ + private static final String ERROR_HINT = """ hint: the last statement in the script something like: ({ options: { @@ -81,18 +80,18 @@ public static Future get(Path scriptPath, Optional mcl return executor.submit(() -> new NodeService(scriptPath, mclient, conf)); } - private NodeService(Path scriptPath, Optional mclient, Configuration conf) throws IOException { - this.mclient = mclient; - this.conf = conf; - + private NodeService(Path scriptPath, Optional mclient, Configuration config) throws IOException { + super(args(scriptPath, mclient, config)); this.source = Files.readString(scriptPath); this.codeHash = this.source.hashCode(); + } + private static JSServiceArgs args(Path scriptPath, Optional mclient, Configuration config) throws IOException { // check plugin definition - var out = new LinkedBlockingDeque(); - Object[] message = { "parse", this.source, out }; + Object[] message = { "parse", Files.readString(scriptPath), out }; NodeQueue.instance().queue().offer(message); + try { var result = out.take(); @@ -101,105 +100,113 @@ private NodeService(Path scriptPath, Optional mclient, Configuratio var parsed = JsonParser.parseString(result); if (!parsed.isJsonObject()) { - throw new IllegalArgumentException("wrong node plugin, " + errorHint); + throw new IllegalArgumentException("wrong node plugin, " + ERROR_HINT); } var parsedObj = parsed.getAsJsonObject(); if (!parsedObj.has("options")) { - throw new IllegalArgumentException("wrong node plugin, missing member 'options', " + errorHint); + throw new IllegalArgumentException("wrong node plugin, missing member 'options', " + ERROR_HINT); } if (!parsedObj.get("options").isJsonObject()) { - throw new IllegalArgumentException("wrong node plugin, wrong member 'options', " + errorHint); + throw new IllegalArgumentException("wrong node plugin, wrong member 'options', " + ERROR_HINT); } var optionsObj = parsedObj.getAsJsonObject("options"); if (!optionsObj.has("name")) { - throw new IllegalArgumentException("wrong node plugin, missing member 'options.name', " + errorHint); + throw new IllegalArgumentException("wrong node plugin, missing member 'options.name', " + ERROR_HINT); } if (!optionsObj.get("name").isJsonPrimitive() || !optionsObj.get("name").getAsJsonPrimitive().isString()) { - throw new IllegalArgumentException("wrong node plugin, wrong member 'options.name', " + errorHint); + throw new IllegalArgumentException("wrong node plugin, wrong member 'options.name', " + ERROR_HINT); } - this.name = optionsObj.get("name").getAsString(); + var name = optionsObj.get("name").getAsString(); if (!optionsObj.has("description")) { throw new IllegalArgumentException( - "wrong node plugin, missing member 'options.description', " + errorHint); + "wrong node plugin, missing member 'options.description', " + ERROR_HINT); } if (!optionsObj.get("description").isJsonPrimitive() || !optionsObj.get("description").getAsJsonPrimitive().isString()) { throw new IllegalArgumentException( - "wrong node plugin, wrong member 'options.description', " + errorHint); + "wrong node plugin, wrong member 'options.description', " + ERROR_HINT); } - this.description = optionsObj.get("description").getAsString(); + var description = optionsObj.get("description").getAsString(); if (!optionsObj.has("uri")) { - throw new IllegalArgumentException("wrong node plugin, missing member 'options.uri', " + errorHint); + throw new IllegalArgumentException("wrong node plugin, missing member 'options.uri', " + ERROR_HINT); } if (!optionsObj.get("uri").isJsonPrimitive() || !optionsObj.get("uri").getAsJsonPrimitive().isString()) { - throw new IllegalArgumentException("wrong node plugin, wrong member 'options.uri', " + errorHint); + throw new IllegalArgumentException("wrong node plugin, wrong member 'options.uri', " + ERROR_HINT); } if (!optionsObj.get("uri").getAsString().startsWith("/")) { - throw new IllegalArgumentException("wrong node plugin, wrong member 'options.uri', " + errorHint); + throw new IllegalArgumentException("wrong node plugin, wrong member 'options.uri', " + ERROR_HINT); } - this.uri = optionsObj.get("uri").getAsString(); + var uri = optionsObj.get("uri").getAsString(); + + boolean secured; if (!optionsObj.has("secured")) { - this.secured = false; + secured = false; } else { if (!optionsObj.get("secured").isJsonPrimitive() || !optionsObj.get("secured").getAsJsonPrimitive().isBoolean()) { throw new IllegalArgumentException( - "wrong node plugin, wrong member 'options.secured', " + errorHint); + "wrong node plugin, wrong member 'options.secured', " + ERROR_HINT); } else { - this.secured = optionsObj.get("secured").getAsBoolean(); + secured = optionsObj.get("secured").getAsBoolean(); } } + MATCH_POLICY matchPolicy; + if (!optionsObj.has("matchPolicy")) { - this.matchPolicy = MATCH_POLICY.PREFIX; + matchPolicy = MATCH_POLICY.PREFIX; } else { if (!optionsObj.get("matchPolicy").isJsonPrimitive() || !optionsObj.get("matchPolicy").getAsJsonPrimitive().isString()) { throw new IllegalArgumentException( - "wrong node plugin, wrong member 'options.secured', " + errorHint); + "wrong node plugin, wrong member 'options.secured', " + ERROR_HINT); } else { var _matchPolicy = optionsObj.get("matchPolicy").getAsString(); try { - this.matchPolicy = MATCH_POLICY.valueOf(_matchPolicy); + matchPolicy = MATCH_POLICY.valueOf(_matchPolicy); } catch (Throwable t) { throw new IllegalArgumentException( - "wrong node plugin, wrong member 'options.matchPolicy', " + errorHint); + "wrong node plugin, wrong member 'options.matchPolicy', " + ERROR_HINT); } } } if (!parsedObj.has("handle")) { - throw new IllegalArgumentException("wrong js plugin, missing member 'handle', " + errorHint); + throw new IllegalArgumentException("wrong js plugin, missing member 'handle', " + ERROR_HINT); } - if (!parsedObj.get("handle").isJsonPrimitive() || !parsedObj.get("handle").getAsJsonPrimitive().isString() - || !"function".equals(parsedObj.get("handle").getAsString())) { - throw new IllegalArgumentException("wrong js plugin, member 'handle' is not a function, " + errorHint); + if (!parsedObj.get("handle").isJsonPrimitive() || !parsedObj.get("handle").getAsJsonPrimitive().isString() || !"function".equals(parsedObj.get("handle").getAsString())) { + throw new IllegalArgumentException("wrong js plugin, member 'handle' is not a function, " + ERROR_HINT); } + + return new JSServiceArgs(name, description, uri, secured, null, matchPolicy, null, config, mclient, new HashMap()); } catch (InterruptedException ie) { LOGGER.debug("Error initializing node plugin", ie); Thread.currentThread().interrupt(); } + + return null; } /** * */ + @Override public void handle(StringRequest request, StringResponse response) { var out = new LinkedBlockingDeque(); Object[] message = { "handle", @@ -207,16 +214,16 @@ public void handle(StringRequest request, StringResponse response) { request, response, out, LOGGER, // pass LOGGER to node runtime - this.mclient, // pass mclient to node runtime - this.conf == null // pass pluginArgs to node runtime - ? Maps.newHashMap() : this.conf.getOrDefault(this.name, Maps.newHashMap()) + mclient(), // pass mclient to node runtime + configuration() == null // pass pluginArgs to node runtime + ? Maps.newHashMap() : configuration().getOrDefault(name(), Maps.newHashMap()) }; try { NodeQueue.instance().queue().offer(message); var result = out.take(); - if (result instanceof RuntimeException) { - throw ((RuntimeException) result); + if (result instanceof RuntimeException runtimeException) { + throw runtimeException; } else { LOGGER.debug("handle result: {}", result); } diff --git a/polyglot/src/main/resources/META-INF/native-image/org.restheart/restheart-polyglot/reflect-config.json b/polyglot/src/main/resources/META-INF/native-image/org.restheart/restheart-polyglot/reflect-config.json index ac3f3960b1..969329c321 100644 --- a/polyglot/src/main/resources/META-INF/native-image/org.restheart/restheart-polyglot/reflect-config.json +++ b/polyglot/src/main/resources/META-INF/native-image/org.restheart/restheart-polyglot/reflect-config.json @@ -1,6 +1,6 @@ [ { - "name": "org.restheart.polyglot.AbstractJSPlugin", + "name": "org.restheart.polyglot.JSPlugin", "fields": [{ "name": "interceptPoint" }, { "name": "name" }] }, {