Java 8 Nashorn and DustJS

In this post we will show how to use java 8 nashorn, a JavaScript engine developed in the Java programming language, will be used to compile, load and render a template using dustjs.

DustJS is a JavaScript templating engine designed to provide clean separation between presentation logic and isn't constrained how the data is provided to it. It is different than other java template frameworks such as velocity, freemarker, jsps and thymeleaf as it doesn't require a heavy java server running in the background. The project gained its popularity when linkedin cloned dustjs and started providing full backing and support. You can read view client side templating throwdown on their comparison between templating frameworks.

Why?

Why would you want to render a javascript template or javascript file on the JVM?

  1. If you have core business logic written in javascript that you need to execute and access in java.
  2. As application architectures progress, it is important to support application developer roles. It used to be ok to have a designer learn a templating engine and force them to use a heavy J2EE servers and tools. As the front end engineer roles continue to become prominent we need to be creative and support ways they can work with the right tools.
  3. Being able to render a template on the server or client side gives you flexibility. It allows you to create a site completely disconnected from the server by using mock data. This is helpful when you are trying to put together prototypes or ship off production demos.
  4. It allows roles to be working in parallel. While a front end developer is working on creating a production ready front end, a middle tier developer can be working on assembling or piecing together data. An important piece in this model is that roles must work together on defining a data contract.

Create Nashhorn Script Engine

In order to evaluate JavaScript code from java, you need to create a nashorn script engine by utilizing the javax.script package. By calling ScriptEngineManager.getEngineByName we will gain reference to the nashorn ScriptEngine. Next we will load dustjs library by using a FileReader to pass the source of the script to be executed.

ScriptEngineManager engineManager = new ScriptEngineManager();
ScriptEngine engine = engineManager.getEngineByName("nashorn");
engine.eval(new FileReader(
    "src/test/resources/com/levelup/js/dust-full-2.3.5.js"));

Compile template

Once we load the dustjs library we need gain reference to the dust engine by calling the engine.eval("dust"). Next we will compile a simple template to a JavaScript string that will register the template named "myphrase".

// evaluate dustjs in nashorn
Invocable invocable = (Invocable) engine;
Object dustjs = engine.eval("dust");

// compile and register template
String template = "Hello {name}! You have {count} new messages.";
Object compileTemplate = invocable.invokeMethod(dustjs, "compile",
        template, "myphrase");

If you were to execute this with straight up JavaScript, it would look like:

var compileTemplate = dust.compile("Hello {name}! You have {count} new messages.", "myphrase");

Load or register template to dust context

Immediately following the compile process we will register the compiled template with the dust engine.

// load template into dust context
Object loadedSource = invocable.invokeMethod(dustjs, "loadSource",
        compileTemplate);

If you were to execute this with straight up JavaScript, it would look like:

dust.loadSource(compileTemplate);

Render template

There is a four steps to render or mesh data with the template.

Define javascript function

After compiling and registering the template with the dust engine we can render the template by defining a string that contains the JavaScript expression to execute. Calling the dust render function we will pass the template, JSON model and a call back function; where data is the rendered output from the running the template and err has any error information. Once the JavaScript function is processed we want the result to come back to java. We can do this by invoking a java method in JavaScript by passing java's StringWriter into the script context. Upon successful render the output will be sent to StringWriter.

Writer writer = new StringWriter();

String renderScript = ("{dust.render(name, " + 
    "JSON.parse(json), "
    + "function(err,data) { " 
    + "if(err) { "
        + "throw new Error(err);" 
    + "} " 
    + "else { "
        + "writer.write( data, 0, data.length );" 
    + "}  " 
    + "});}");

Define data to pass to the template

String nameJson = "{\"name\": \"Mick\",\"count\": 30}";

Bind objects to js engine

Create a Bindings object and put elements that should be bound to nashorn.

Bindings bindings = new SimpleBindings();
bindings.put("name", "myphrase");
bindings.put("json", nameJson);
bindings.put("writer", writer);

Execute it!

engine.getContext().setBindings(bindings, ScriptContext.GLOBAL_SCOPE);

engine.eval(renderScript, engine.getContext());

Output

Hello Mick! You have 30 new messages.

A verbose way to execute the following render step in JavaScript, it would look like:

dust.render("myphrase", JSON.parse({\"name\": \"Mick\",\"count\": 30}), function(err, data) {
    console.log(data);
});

All together

ScriptEngineManager engineManager = new ScriptEngineManager();
ScriptEngine engine = engineManager.getEngineByName("nashorn");
engine.eval(new FileReader(
        "src/test/resources/com/levelup/js/dust-full-2.3.5.js"));

Invocable invocable = (Invocable) engine;
Object dustjs = engine.eval("dust");

String template = "Hello {name}! You have {count} new messages.";
Object compileTemplate = invocable.invokeMethod(dustjs, "compile",
        template, "myphrase");

Object loadedSource = invocable.invokeMethod(dustjs, "loadSource",
        compileTemplate);

String renderScript = ("{dust.render(name, " + 
        "JSON.parse(json), "
        + "function(err,data) { " 
        + "if(err) { "
            + "throw new Error(err);" 
        + "} " 
        + "else { "
            + "writer.write( data, 0, data.length );" 
        + "}  " 
        + "});}");

Writer writer = new StringWriter();

String nameJson = "{\"name\": \"Mick\",\"count\": 30}";

Bindings bindings = new SimpleBindings();
bindings.put("name", "myphrase");
bindings.put("json", nameJson);
bindings.put("writer", writer);

engine.getContext().setBindings(bindings, ScriptContext.GLOBAL_SCOPE);

engine.eval(renderScript, engine.getContext());

System.out.println(writer);

Gotcha

At first I defined the render script below, notice the line writer.write( data ).

String renderScript =
    ("{dust.render(name, " +
    "JSON.parse(json), " +
    "function(err,data) { " +
        "if(err) { " +
       "throw new Error(err);" +
     "} " +
     "else { " +
        "writer.write( data );" + 
      "}  " +
    "});}");

StringWriter has a few overloaded methods which I assumed by default it would call write( string ). When running I received the error below which trigger the change to write(char[] cbuf, int off, int len)

Caused by: java.lang.NoSuchMethodException: Can't unambiguously select between fixed arity signatures [(int), (java.lang.String)] of the method java.io.StringWriter.write for argument types [jdk.nashorn.internal.runtime.ConsString]
    at jdk.internal.dynalink.beans.OverloadedMethod.throwAmbiguousMethod(OverloadedMethod.java:222)
    at jdk.nashorn.internal.scripts.Script$\^eval\_._L1(<eval>:1)
    at jdk.nashorn.internal.scripts.Script$\^eval\_._L4$_L456(<eval>:473)
    at jdk.nashorn.internal.scripts.Script$\^eval\_._L4$_L578(<eval>:583)
    at jdk.nashorn.internal.scripts.Script$\^eval\_._L4$_L97(<eval>:100)
    ... 33 more

That's it

To some it might feel unnatural calling JavaScript via Java but as I described above it can give you the flexibility to build creative solutions. In addition, I hope this post was helpful in showing how to render dustjs template using the nashorn engine.