diff --git a/handlebars-maven-plugin/pom.xml b/handlebars-maven-plugin/pom.xml index 2e6693e8..6933104e 100644 --- a/handlebars-maven-plugin/pom.xml +++ b/handlebars-maven-plugin/pom.xml @@ -21,6 +21,12 @@ ${project.version} + + org.openjdk.nashorn + nashorn-core + 15.4 + + ch.qos.logback logback-classic diff --git a/handlebars/pom.xml b/handlebars/pom.xml index 581a14e8..f194c83a 100644 --- a/handlebars/pom.xml +++ b/handlebars/pom.xml @@ -170,6 +170,23 @@ org.openjdk.nashorn nashorn-core ${nashorn.version} + true + + + + org.graalvm.js + js-scriptengine + ${graaljs.version} + true + + + + org.graalvm.polyglot + js + ${graaljs.version} + pom + runtime + true @@ -240,6 +257,7 @@ 15.4 + 25.0.3 @@ -300,5 +318,55 @@ + + + graaljs-jit + + + org.graalvm.compiler + compiler + ${graaljs.version} + test + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + graal-compiler-path + initialize + + properties + + + + copy-graaljs-modules + generate-test-resources + + copy-dependencies + + + ${project.build.directory}/graaljs-modules + org.graalvm.polyglot,org.graalvm.js,org.graalvm.truffle,org.graalvm.sdk,org.graalvm.regex,org.graalvm.shadowed + compiler + pom + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + -Duser.language=en -Duser.country=US -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --module-path=${project.build.directory}/graaljs-modules --add-modules=org.graalvm.polyglot --upgrade-module-path=${org.graalvm.compiler:compiler:jar} --add-exports=org.graalvm.truffle.compiler/com.oracle.truffle.compiler=jdk.graal.compiler --add-exports=org.graalvm.truffle.compiler/com.oracle.truffle.compiler.hotspot=jdk.graal.compiler --add-exports=org.graalvm.truffle.compiler/com.oracle.truffle.compiler.hotspot.libgraal=jdk.graal.compiler --add-exports=org.graalvm.word/org.graalvm.word.impl=jdk.graal.compiler + + + + + diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/Handlebars.java b/handlebars/src/main/java/com/github/jknack/handlebars/Handlebars.java index fc257e2c..bfb20d00 100644 --- a/handlebars/src/main/java/com/github/jknack/handlebars/Handlebars.java +++ b/handlebars/src/main/java/com/github/jknack/handlebars/Handlebars.java @@ -27,7 +27,6 @@ import javax.script.Bindings; import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; import org.slf4j.Logger; @@ -38,6 +37,7 @@ import com.github.jknack.handlebars.internal.Files; import com.github.jknack.handlebars.internal.FormatterChain; import com.github.jknack.handlebars.internal.HbsParserFactory; +import com.github.jknack.handlebars.internal.ScriptEngineFactory; import com.github.jknack.handlebars.internal.Throwing; import com.github.jknack.handlebars.io.ClassPathTemplateLoader; import com.github.jknack.handlebars.io.CompositeTemplateLoader; @@ -1442,19 +1442,21 @@ private static EscapingStrategy newEscapeChain(final EscapingStrategy[] chain) { } /** - * @return Nashorn engine. + * @return A JavaScript engine (GraalJS or Nashorn, whichever is available). */ private ScriptEngine engine() { synchronized (this) { if (this.engine == null) { - this.engine = new ScriptEngineManager().getEngineByName("nashorn"); + this.engine = ScriptEngineFactory.create(); - Throwing.run(() -> { - //polyfill globalThis as it is used in handlebars 4.7.9 and is not supported by nashorn - engine.eval("var globalThis = this;"); - engine.eval(Files.read(this.handlebarsJsFile, charset)); - }); + Throwing.run( + () -> { + if (ScriptEngineFactory.isNashorn(engine)) { + engine.eval("var globalThis = this;"); + } + engine.eval(Files.read(this.handlebarsJsFile, charset)); + }); } return this.engine; } diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/helper/DefaultHelperRegistry.java b/handlebars/src/main/java/com/github/jknack/handlebars/helper/DefaultHelperRegistry.java index e8088787..66480d9d 100644 --- a/handlebars/src/main/java/com/github/jknack/handlebars/helper/DefaultHelperRegistry.java +++ b/handlebars/src/main/java/com/github/jknack/handlebars/helper/DefaultHelperRegistry.java @@ -27,7 +27,6 @@ import java.util.regex.Pattern; import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +36,7 @@ import com.github.jknack.handlebars.Helper; import com.github.jknack.handlebars.HelperRegistry; import com.github.jknack.handlebars.internal.Files; +import com.github.jknack.handlebars.internal.ScriptEngineFactory; import com.github.jknack.handlebars.internal.Throwing; /** @@ -175,7 +175,9 @@ public HelperRegistry registerHelpers(final String filename, final String source notNull(filename, "The filename is required."); notEmpty(source, "The source is required."); ScriptEngine engine = engine(); - Throwing.run(() -> engine.eval(adaptES6Literals(source))); + String adaptedSource = + ScriptEngineFactory.isNashorn(engine) ? adaptES6Literals(source) : source; + Throwing.run(() -> engine.eval(adaptedSource)); return this; } @@ -276,12 +278,12 @@ public DefaultHelperRegistry setCharset(final Charset charset) { } /** - * @return Nashorn engine. + * @return A JavaScript engine (GraalJS or Nashorn, whichever is available). */ private ScriptEngine engine() { synchronized (this) { if (this.engine == null) { - this.engine = new ScriptEngineManager().getEngineByName("nashorn"); + this.engine = ScriptEngineFactory.create(); this.engine.put("Handlebars_java", this); diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/internal/ScriptEngineFactory.java b/handlebars/src/main/java/com/github/jknack/handlebars/internal/ScriptEngineFactory.java new file mode 100644 index 00000000..5d37ecdb --- /dev/null +++ b/handlebars/src/main/java/com/github/jknack/handlebars/internal/ScriptEngineFactory.java @@ -0,0 +1,101 @@ +/* + * Handlebars.java: https://github.com/jknack/handlebars.java + * Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.github.jknack.handlebars.internal; + +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; + +/** + * Factory for creating a JavaScript {@link ScriptEngine}. By default tries Nashorn first, then + * falls back to GraalJS. The engine can be forced via the {@code hbs.js_engine} system property. + */ +public final class ScriptEngineFactory { + + private ScriptEngineFactory() {} + + /** + * Create a JavaScript engine. If the system property {@code hbs.js_engine} is set, only that + * engine is tried. Otherwise tries Nashorn first, then GraalJS. + * + * @return a JavaScript ScriptEngine. + * @throws IllegalStateException if no JavaScript engine is available or the requested engine is + * not found. + */ + public static ScriptEngine create() { + String requested = System.getProperty("hbs.js_engine"); + if (requested != null) { + return createRequested(requested); + } + return createDefault(); + } + + private static ScriptEngine createDefault() { + ScriptEngineManager manager = new ScriptEngineManager(); + + ScriptEngine engine = manager.getEngineByName("nashorn"); + if (engine != null) { + return engine; + } + + engine = manager.getEngineByName("graal.js"); + if (engine != null) { + configureGraalJS(engine); + return engine; + } + + throw new IllegalStateException( + "No JavaScript engine found. Add either GraalJS or Nashorn to the classpath."); + } + + private static ScriptEngine createRequested(String name) { + ScriptEngineManager manager = new ScriptEngineManager(); + + switch (name) { + case "nashorn": + ScriptEngine nashorn = manager.getEngineByName("nashorn"); + if (nashorn == null) { + throw new IllegalStateException( + "JavaScript engine 'nashorn' requested via hbs.js_engine but is not available." + + " Add org.openjdk.nashorn:nashorn-core to the classpath."); + } + return nashorn; + + case "graaljs": + ScriptEngine graaljs = manager.getEngineByName("graal.js"); + if (graaljs == null) { + throw new IllegalStateException( + "JavaScript engine 'graaljs' requested via hbs.js_engine but is not available." + + " Add org.graalvm.js:js-scriptengine to the classpath."); + } + configureGraalJS(graaljs); + return graaljs; + + default: + throw new IllegalStateException( + "Unknown hbs.js_engine value: '" + + name + + "'. Supported values are 'nashorn' and 'graaljs'."); + } + } + + private static void configureGraalJS(ScriptEngine engine) { + Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); + bindings.put("polyglot.js.allowAllAccess", true); + bindings.put("polyglot.js.nashorn-compat", true); + bindings.put("polyglot.js.ecmascript-version", "2022"); + } + + /** + * Check whether the given engine is Nashorn. + * + * @param engine the script engine. + * @return true if the engine is Nashorn. + */ + public static boolean isNashorn(ScriptEngine engine) { + return engine.getFactory().getEngineName().toLowerCase().contains("nashorn"); + } +} diff --git a/handlebars/src/test/java/com/github/jknack/handlebars/NashornTest.java b/handlebars/src/test/java/com/github/jknack/handlebars/NashornTest.java index 218db605..2d9b8cc8 100644 --- a/handlebars/src/test/java/com/github/jknack/handlebars/NashornTest.java +++ b/handlebars/src/test/java/com/github/jknack/handlebars/NashornTest.java @@ -13,11 +13,11 @@ import java.util.Map; import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; import javax.script.SimpleBindings; import org.junit.jupiter.api.Test; +import com.github.jknack.handlebars.internal.ScriptEngineFactory; import com.github.jknack.handlebars.js.JavaScriptHelperTest; public class NashornTest { @@ -45,13 +45,13 @@ public String getName() { @Test public void bootstrap() throws Exception { Handlebars hbs = new Handlebars(); - ScriptEngine nashorn = new ScriptEngineManager().getEngineByName("nashorn"); + ScriptEngine engine = ScriptEngineFactory.create(); SimpleBindings bindings = new SimpleBindings(); bindings.put("Handlebars_java", hbs); - nashorn.eval( + engine.eval( new FileReader(Paths.get("src/main/resources/helpers.nashorn.js").toFile()), bindings); - nashorn.eval( + engine.eval( new FileReader( Paths.get("src/test/resources/com/github/jknack/handlebars/js/helpers.js").toFile()), bindings);