Using Javascript as a plugin tool for Java

Let’s have fun with Javascript today… Inside Java.

Goal

Javascript is a powerfull and flexible language, while Java can be a lot more strict. The combinaison of both can be quite interesting, and offer (for example) an easy and cheap plugin solution for Java. Moreover, Java got everything by default to run Javascript using Mozilla Rhino engine.
On the other side, I don’t see that much resource on the web talking about it, while it’s really not a difficult system to implement.

Time to code!

Engine

As everything in Java for such system, we start with a factory handling many engine system, so it means you can use something different than Javascript, but remind that you will probably need an Invokable scripting engine, and it seems not all of them support this…
After creating the engine, we would like of course to have bindings to send data to script system (like Java object, int value, …), finally, we want also to be able to call function with parameters.
In less than 200 lines of code, we can achieve a generic class handling all cases:

package com.simplapi.rhino;

import java.io.Reader;
import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

/**
 * Simple Javascript function caller
 * 
 * @author Deisss (MIT License)
 * @version 0.1
 */
public class Javascript {
	private String script;
	private Reader reader;
	private ScriptEngine engine;
	private Bindings bindings;

	/**
	 * Constructor
	 * 
	 * @param script The script to start
	 */
	public Javascript(String script) {
		this.startEngine();
		this.setScript(script);
	}

	/**
	 * Constructor
	 * 
	 * @param reader An alternative to script using Reader
	 */
	public Javascript(Reader reader) {
		this.startEngine();
		this.setReader(reader);
	}

	/**
	 * Constructor
	 */
	public Javascript() {
		this.startEngine();
	}

	/**
	 * Start engine system
	 */
	private void startEngine() {
		this.script = null;
		this.reader = null;
		ScriptEngineManager manager = new ScriptEngineManager();
		this.engine = manager.getEngineByName("javascript");
		this.bindings = this.engine.getBindings(ScriptContext.ENGINE_SCOPE);
	}

	/**
	 * Register a new variable to use inside the system
	 * 
	 * @param name The name to use inside script
	 * @param content The object value (can be java element)
	 */
	public void variable(String name, Object content) {
		this.bindings.put(name, content);
	}

	/**
	 * Run a single eval, without function, and return content
	 * 
	 * @return The object created inside your script
	 * @throws ScriptException 
	 */
	public Object run() throws ScriptException {
		if(this.script == null && this.reader == null) {
			return null;
		}
		if(this.reader != null) {
			return this.engine.eval(this.reader);
		} else {
			return this.engine.eval(this.script);
		}
	}

	/**
	 * Call a function with scope binding, and get function return
	 * 
	 * @param fct The function name to call (should appear inside the script
	 *                                                            submitted)
	 * @param args The arguments to pass to this function
	 * @return The javascript function return
	 * @throws ScriptException
	 * @throws NoSuchMethodException
	 */
	public Object run(String fct, Object... args)
			throws ScriptException, NoSuchMethodException {

		// Stop if not possible to run
		if(this.script == null && this.reader == null) {
			return null;
		}
		// Starting script
		this.run();

		// Starting invoke element
		Invocable invoke = (Invocable) this.engine;
		return invoke.invokeFunction(fct, args);
	}

	/**
	 * Clear the script currently loaded and readers
	 */
	public void clear() {
		this.reader = null;
		this.script = null;
	}

	/**
	 * Set the script to pun inside system
	 * 
	 * @param script The script to run
	 */
	public final void setScript(String script) {
		if(script != null && script.length() > 0) {
			this.script = script;
		}
	}

	/**
	 * Get the script to run
	 * 
	 * @return The script actually setted
	 */
	public String getScript() {
		return this.script;
	}

	/**
	 * Set the reader (alternative to script)
	 * 
	 * @param reader The reader to use when eval js code
	 */
	public final void setReader(Reader reader) {
		if(reader != null) {
			this.reader = reader;
		}
	}

	/**
	 * Get the reader
	 * 
	 * @return The reader currently in use
	 */
	public Reader getReader() {
		return this.reader;
	}

	/**
	 * Get the internal bindings currently in use
	 * 
	 * @return The system currently in use
	 */
	public Bindings getBindings() {
		return this.bindings;
	}

	/**
	 * Get the engine currently in use
	 * 
	 * @return The engine currently in use
	 */
	public ScriptEngine getEngine() {
		return this.engine;
	}
}

Above we define an engine and a bindings (for global variables pass), and we provide few functions, the more interesting here are the variable function (to add a global variable before running script), and run to run a simple code, or function.
From that point, we can run a simple script, but also a more complex function with parameters bindings, let’s check few examples.

Note: I’ve found that the engine, on return element, really dislike Integer values and always seems to prefer Double instead…

Examples

First example

The first example would be a really classic one, a closure, and a return:

Javascript js = new Javascript();
js.setScript("(function() {return 1 + x;})();");
js.variable("x", 10);

Double result = (Double) js.run();
System.out.println("Script result (attended: 11) = " + result);

You will probably found a result of ’11’ as expected on console. As you see the ‘x’ variable has been setted into global scope, which makes it available inside function.

Second example

The second one is already little bit more complex as we define function with parameters:

Javascript js = new Javascript();

js.setScript("function test(another) {return 1 + x - another;}");
js.variable("x", 10);

Double result = (Double) js.run("test", 5);
System.out.println("Function result (attended: 6) = " + result);

Here we mix function, global scope variable, function variable, and as you may see the result is the good one.

Thrid example

In this example, we define a Java object, and send it as parameter, we do also the same with System.out to see how you can bind Java element to script:

Javascript js = new Javascript();

String script = "function test(obj, another) {";
script += "out.println('Debug: ' + typeof(obj.x));";
script += "return 1 + obj.x - another;}";
js.setScript(script);
js.variable("out", System.out);

Double result = (Double) js.run("test", new TestObject(), 2);
System.out.println("Function result (attended: 3) = " + result);

And the java TestObject class:

public class TestObject {
	public int x = 4;
}

As you see the out function is used inside Javascript and appear in regular Java console, while the obj.x is handled as expected.

Important note: You may found wrong behavior using this technique, for example, here on TestObject if you replace the int by Integer (for the variable x), you will not found as result 3 but 12. This is because the Integer is consider on Javascript side as an object, which means 1 + obj(4) will gives 14 instead of 5.
As you see, it does work for as expected in many cases, but an error due to type conversion can appears easily.

Fourth example

For now we try mostly integer/double values, so let’s try boolean instead:

Javascript js = new Javascript();
js.setScript("function test() {return true;}");

Boolean result = (Boolean) js.run("test");
System.out.println("Function result (attended: true) = " + result);

As expected we found the good value (true), which means that for basic type, Rhino and Java can work great and do convertion for your needs. It does also means that basic type should be prefered in many cases to complex type as we see above.

Fifth example

Let’s create a lot more complex one. You probably know a strong difference between Javascript and Java: JSON. Javascript is FAR AWAY more cool for handling JSON, so let’s try to create a parser for JSON!

First of all, your parser system:

function extract(chain) {
  var arr = chain.split('.'),
      obj = JSON.parse(body),
      current = obj;
  for(var i=0, l=arr.length; i<l; ++i) {
    if(arr[i] in current) {
      current = current[arr[i]];
    }
  }
  return current;
}

This function is able, from a chain like this ‘ok.ok2’ to extract from this object ‘{« ok »: {« ok2 »: « something »}’ the word ‘something’. As the ‘.’ is used as a separator to search inside sub object. We use a global variable ‘body’ to handle string JSON version.
If you try to run this example, you will have trouble: the JSON.parse is not defined! So let’s grab a JSON parser json3 (I personnaly use the production version). Then we need to load it first to get access to JSON:

Javascript js = new Javascript();
// Adding json support
js.setReader(new FileReader("c:/json3.js"));
js.run();
// Clear to remove reader as main source
js.clear();

// Trying parsing method
js.variable("body", "{\"ok\": {\"ok2\":\"something\"}}");
js.setScript("function extract(chain) {var arr = chain.split('.'),obj = JSON.parse(body),current = obj;for(var i=0, l=arr.length; i<l; ++i) {if(arr[i] in current) {current = current[arr[i]];}}return current;}");
String result = (String) js.run("extract", "ok.ok2");
System.out.println("Function result (attended: 'something') = " + result);

We suppose of course json3.js to be at the root of C:\ drive (on Windows). As you see, handling JSON can create a wide range of possibilities for Java for extracting single value from complex JSON files without knowing everything from it like most of Java parser need.

Final words

It was a quick introduction to Javascript and Java communication. We use here the script version (for keeping short), but you can also submit script using java.io.Reader (using setReader instead of setScript), which makes file loading easy to use (and so, a plugin system!).

I hope you will soon use this into your own system!

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :