diff --git a/api/src/main/java/com/instancify/scriptify/api/script/CompiledScript.java b/api/src/main/java/com/instancify/scriptify/api/script/CompiledScript.java new file mode 100644 index 0000000..aabffe2 --- /dev/null +++ b/api/src/main/java/com/instancify/scriptify/api/script/CompiledScript.java @@ -0,0 +1,31 @@ +package com.instancify.scriptify.api.script; + +/** + * Defines the structure of a compiled script that can be executed. + * + * @param type of value returned by the script after evaluation + */ +public interface CompiledScript extends AutoCloseable { + + /** + * Retrieves the value produced by the script after compilation. + * + * @return the compiled script value + */ + T get(); + + /** + * Executes the compiled script with the provided arguments. + * + * @param args arguments passed to the script during execution + * @return the result of script execution + */ + T eval(Object... args); + + /** + * Closes the compiled script and releases any resources associated with it. + */ + @Override + void close(); +} + diff --git a/api/src/main/java/com/instancify/scriptify/api/script/Script.java b/api/src/main/java/com/instancify/scriptify/api/script/Script.java index 60036ed..87a3355 100644 --- a/api/src/main/java/com/instancify/scriptify/api/script/Script.java +++ b/api/src/main/java/com/instancify/scriptify/api/script/Script.java @@ -61,9 +61,34 @@ public interface Script { void addExtraScript(String script); /** - * Evaluates and executes this script. + * Compiles the script. * - * @throws ScriptFunctionException If there's an error during script evaluation + * @param script the script to compile + * @return compiled script object + * @throws ScriptException if there's an error during script compiling */ - T eval(String script) throws ScriptException; + CompiledScript compile(String script) throws ScriptException; + + /** + * One-time evaluation of a script without worrying about closing the context. + * + * @param script the script to evaluate + * @return the evaluation result + * @throws ScriptException if there's an error during script evaluation + */ + T evalOneShot(String script) throws ScriptException; + + /** + * Evaluates and executes the script. + * + * @param script the script to evaluate + * @return the evaluation result + * @throws ScriptFunctionException if there's an error during script evaluation + * @deprecated For a one-time evaluation of a script, use the Script#evalOneShot method; + * for pre-compiling a script, use Script#compile. + */ + @Deprecated + default T eval(String script) throws ScriptException { + return this.evalOneShot(script); + } } diff --git a/build.gradle.kts b/build.gradle.kts index c559b2e..2950fea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ java { allprojects { group = "com.instancify.scriptify" - version = "1.4.4-SNAPSHOT" + version = "1.5.0-SNAPSHOT" } subprojects { diff --git a/script-js-graalvm/src/main/java/com/instancify/scriptify/js/graalvm/script/JsCompiledScript.java b/script-js-graalvm/src/main/java/com/instancify/scriptify/js/graalvm/script/JsCompiledScript.java new file mode 100644 index 0000000..17712c5 --- /dev/null +++ b/script-js-graalvm/src/main/java/com/instancify/scriptify/js/graalvm/script/JsCompiledScript.java @@ -0,0 +1,34 @@ +package com.instancify.scriptify.js.graalvm.script; + +import com.instancify.scriptify.api.script.CompiledScript; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Value; + +public class JsCompiledScript implements CompiledScript { + private final Context context; + private final Value value; + + public JsCompiledScript(Context context, Value value) { + this.context = context; + this.value = value; + } + + @Override + public Value get() { + return value; + } + + @Override + public Value eval(Object... args) { + if (!value.canExecute()) { + throw new IllegalStateException("Script is not executable"); + } + return value.execute(args); + } + + @Override + public void close() { + context.close(); + } +} + diff --git a/script-js-graalvm/src/main/java/com/instancify/scriptify/js/graalvm/script/JsScript.java b/script-js-graalvm/src/main/java/com/instancify/scriptify/js/graalvm/script/JsScript.java index aad17fc..9ac57c5 100644 --- a/script-js-graalvm/src/main/java/com/instancify/scriptify/js/graalvm/script/JsScript.java +++ b/script-js-graalvm/src/main/java/com/instancify/scriptify/js/graalvm/script/JsScript.java @@ -1,6 +1,7 @@ package com.instancify.scriptify.js.graalvm.script; import com.instancify.scriptify.api.exception.ScriptException; +import com.instancify.scriptify.api.script.CompiledScript; import com.instancify.scriptify.api.script.Script; import com.instancify.scriptify.api.script.ScriptObject; import com.instancify.scriptify.api.script.constant.ScriptConstant; @@ -57,7 +58,7 @@ public void addExtraScript(String script) { } @Override - public Value eval(String script) throws ScriptException { + public CompiledScript compile(String script) throws ScriptException { Context.Builder builder = Context.newBuilder("js") .allowHostAccess(HostAccess.newBuilder(HostAccess.ALL) // Mapping for the ScriptObject class required @@ -98,11 +99,16 @@ public Value eval(String script) throws ScriptException { fullScript.append(script); try { - return context.eval("js", fullScript.toString()); + return new JsCompiledScript(context, context.eval("js", fullScript.toString())); } catch (Exception e) { throw new ScriptException(e); - } finally { - context.close(); + } + } + + @Override + public Value evalOneShot(String script) throws ScriptException { + try (CompiledScript compiled = compile(script)) { + return compiled.get(); } } } diff --git a/script-js-rhino/src/main/java/com/instancify/scriptify/js/rhino/script/JsCompiledScript.java b/script-js-rhino/src/main/java/com/instancify/scriptify/js/rhino/script/JsCompiledScript.java new file mode 100644 index 0000000..d529e35 --- /dev/null +++ b/script-js-rhino/src/main/java/com/instancify/scriptify/js/rhino/script/JsCompiledScript.java @@ -0,0 +1,39 @@ +package com.instancify.scriptify.js.rhino.script; + +import com.instancify.scriptify.api.script.CompiledScript; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Function; +import org.mozilla.javascript.Script; +import org.mozilla.javascript.ScriptableObject; + +public class JsCompiledScript implements CompiledScript { + + private final Context context; + private final ScriptableObject scope; + private final Script compiled; + + public JsCompiledScript(Context context, ScriptableObject scope, Script compiled) { + this.context = context; + this.scope = scope; + this.compiled = compiled; + } + + @Override + public Object get() { + return compiled.exec(context, scope); + } + + @Override + public Object eval(Object... args) { + Object result = compiled.exec(context, scope); + if (result instanceof Function function) { + return function.call(context, scope, scope, args); + } + return result; + } + + @Override + public void close() { + context.close(); + } +} diff --git a/script-js-rhino/src/main/java/com/instancify/scriptify/js/rhino/script/JsScript.java b/script-js-rhino/src/main/java/com/instancify/scriptify/js/rhino/script/JsScript.java index 16312a2..10af4c5 100644 --- a/script-js-rhino/src/main/java/com/instancify/scriptify/js/rhino/script/JsScript.java +++ b/script-js-rhino/src/main/java/com/instancify/scriptify/js/rhino/script/JsScript.java @@ -1,6 +1,7 @@ package com.instancify.scriptify.js.rhino.script; import com.instancify.scriptify.api.exception.ScriptException; +import com.instancify.scriptify.api.script.CompiledScript; import com.instancify.scriptify.api.script.Script; import com.instancify.scriptify.api.script.constant.ScriptConstant; import com.instancify.scriptify.api.script.constant.ScriptConstantManager; @@ -19,16 +20,11 @@ public class JsScript implements Script { - private final Context context = Context.enter(); private final ScriptSecurityManager securityManager = new StandardSecurityManager(); private ScriptFunctionManager functionManager = new StandardFunctionManager(); private ScriptConstantManager constantManager = new StandardConstantManager(); private final List extraScript = new ArrayList<>(); - public JsScript() { - context.setWrapFactory(new JsWrapFactory()); - } - @Override public ScriptSecurityManager getSecurityManager() { return securityManager; @@ -60,36 +56,45 @@ public void addExtraScript(String script) { } @Override - public Object eval(String script) throws ScriptException { - ScriptableObject scope = context.initStandardObjects(); - - // If security mode is enabled, search all exclusions - // and add the classes that were excluded to JsSecurityClassAccessor - if (securityManager.getSecurityMode()) { - context.setClassShutter(new JsSecurityClassAccessor(securityManager.getExcludes())); - } - - for (ScriptFunctionDefinition definition : functionManager.getFunctions().values()) { - scope.put(definition.getFunction().getName(), scope, new JsFunction(this, definition)); - } - - for (ScriptConstant constant : constantManager.getConstants().values()) { - ScriptableObject.putConstProperty(scope, constant.getName(), constant.getValue()); - } - - // Building full script including extra script code - StringBuilder fullScript = new StringBuilder(); - for (String extra : extraScript) { - fullScript.append(extra).append("\n"); - } - fullScript.append(script); - + public CompiledScript compile(String script) throws ScriptException { try { - return context.evaluateString(scope, fullScript.toString(), null, 1, null); + Context context = Context.enter(); + context.setWrapFactory(new JsWrapFactory()); + + ScriptableObject scope = context.initStandardObjects(); + + // If security mode is enabled, search all exclusions + // and add the classes that were excluded to JsSecurityClassAccessor + if (securityManager.getSecurityMode()) { + context.setClassShutter(new JsSecurityClassAccessor(securityManager.getExcludes())); + } + + for (ScriptFunctionDefinition definition : functionManager.getFunctions().values()) { + scope.put(definition.getFunction().getName(), scope, new JsFunction(this, definition)); + } + + for (ScriptConstant constant : constantManager.getConstants().values()) { + ScriptableObject.putConstProperty(scope, constant.getName(), constant.getValue()); + } + + // Building full script including extra script code + StringBuilder fullScript = new StringBuilder(); + for (String extra : extraScript) { + fullScript.append(extra).append("\n"); + } + fullScript.append(script); + + var compiled = context.compileString(fullScript.toString(), "script", 1, null); + return new JsCompiledScript(context, scope, compiled); } catch (Exception e) { throw new ScriptException(e); - } finally { - context.close(); + } + } + + @Override + public Object evalOneShot(String script) throws ScriptException { + try (CompiledScript compiled = compile(script)) { + return compiled.get(); } } }