Monday, April 20, 2009

JavaScript in Java

The recent JavaLobby post The Top 10 Unused Features in Java has been extremely popular. At the time of this writing, it is the top ranked post in the DZone Top Links category. In addition a reply to it has been posted as well. There are many interesting observations about underutilized features in Java in both blogs posts and I agree with some more than others. However, item that really caught my attention was the assertion that Java SE 6 is one of the most unused Java features.

I really enjoy working with Java SE 6 and have written about or blogged on Java SE 6 features several times in the past. In this blog posting, I intend to demonstrate a portion of Java SE 6's ability to host execute JavaScript code.

Most Java developers and JavaScript developers understand that besides the four letters "J-A-V-A," JavaScript and Java have very little in common other than some C-like heritage. Still, it can be useful at times to run a scripting language from within Java code and Java SE 6 allows this.

The javax.script package was introduced with Java SE 6 and includes classes, interfaces, and a checked exception related to use of scripting engines within Java. This blog posting will focus on ScriptEngineFactory, ScriptEngineManager, ScriptEngine, and ScriptException.

One of the first things one might want to do is to determine which scripting engines are already available. The next snippet of code shows how easy this is to do with Java SE 6.


final ScriptEngineManager manager = new ScriptEngineManager();
for (final ScriptEngineFactory scriptEngine : manager.getEngineFactories())
{
System.out.println(
scriptEngine.getEngineName() + " ("
+ scriptEngine.getEngineVersion() + ")" );
System.out.println(
"\tLanguage: " + scriptEngine.getLanguageName() + "("
+ scriptEngine.getLanguageVersion() + ")" );
System.out.println("\tCommon Names/Aliases: ");
for (final String engineAlias : scriptEngine.getNames())
{
System.out.println(engineAlias + " ");
}
}


The code shown above generates output like that shown in the next screen snapshot.



As this image demonstrates, the Mozilla Rhino JavaScript engine is included with Sun's Java SE 6. We also see some "common names" that are associated with this particular engine. Any of these names can be used to lookup this engine. In later examples in this post, I will be using the common name "js" for this lookup.

The next code sample will take advantage of the provided Rhino JavaScript engine to execute some JavaScript code from Java code. In this case, we'll be taking advantage of JavaScript's toExponential function.


/**
* Write number in exponential form.
*
* @param numberToWriteInExponentialForm The number to be represented in
* exponential form.
* @param numberDecimalPlaces The number of decimal places to be used in the
* exponential representation.
*/
public static void writeNumberAsExponential(
final Number numberToWriteInExponentialForm,
final int numberDecimalPlaces)
{
final ScriptEngine engine = manager.getEngineByName("js");
try
{
engine.put("inputNumber", numberToWriteInExponentialForm);
engine.put("decimalPlaces", numberDecimalPlaces);
engine.eval("var outputNumber = inputNumber.toExponential(decimalPlaces);");
final String exponentialNumber = (String) engine.get("outputNumber");
System.out.println("Number: " + exponentialNumber);
}
catch (ScriptException scriptException)
{
LOGGER.severe(
"ScriptException encountered trying to write exponential: "
+ scriptException.toString());
}
}


The code above directly invokes JavaScript using the ScriptEngine.eval(String) method to evaluate the provided String containing JavaScript syntax. Before invocation of the eval method, two parameters are "passed in" (bound) to the JavaScript code via ScriptEngine.put(String,Object) calls. The result object of the executed JavaScript is accessed in the Java code using a ScriptEngine.get(String) call.

To demonstrate the above code using the toExponential function, I'll use the following "client" code.


final int sourceNumber = 675456;
writeNumberAsExponential(sourceNumber, 1, System.out);
writeNumberAsExponential(sourceNumber, 2, System.out);
writeNumberAsExponential(sourceNumber, 3, System.out);
writeNumberAsExponential(sourceNumber, 4, System.out);
writeNumberAsExponential(sourceNumber, 5, System.out);


When the above code is run against the writeNumberAsExponential method shown earlier and JavaScript is employed, the output appears similar to that shown in the next screen snapshot.



This example is enough to demonstrate how easy it is to invoke JavaScript functionality from within Java SE 6. However, this could be implemented even more generically as the next two examples will demonstrate. The first example shows invocation of relatively arbitrary JavaScript with no parameters passed/bound and the second example demonstrates invocation of relatively arbitrary JavaScript with parameters passed/bound.

A relatively arbitrary JavaScript string can be processed with code similar to that shown next.


/**
* Process the passed-in JavaScript script that should include an assignment
* to a variable with the name prescribed by the provided nameOfOutput and
* may include parameters prescribed by inputParameters.
*
* @param javaScriptCodeToProcess The String containing JavaScript code to
* be evaluated. This String is not checked for any type of validity and
* might possibly lead to the throwing of a ScriptException, which would
* be logged.
* @param nameOfOutput The name of the output variable associated with the
* provided JavaScript script.
* @param inputParameters Optional map of parameter names to parameter values
* that might be employed in the provided JavaScript script. This map
* may be null if no input parameters are expected in the script.
*/
public static Object processArbitraryJavaScript(
final String javaScriptCodeToProcess,
final String nameOfOutput,
final Map<String, Object> inputParameters)
{
Object result = null;
final ScriptEngine engine = manager.getEngineByName("js");
try
{
if (inputParameters != null)
{
for (final Map.Entry<String,Object> parameter :
inputParameters.entrySet())
{
engine.put(parameter.getKey(), parameter.getValue());
}
}
engine.eval(javaScriptCodeToProcess);
result = engine.get(nameOfOutput);
}
catch (ScriptException scriptException)
{
LOGGER.severe(
"ScriptException encountered trying to write arbitrary JavaScript '"
+ javaScriptCodeToProcess + "': "
+ scriptException.toString());
}
return result;
}


The code above provides quite a bit of flexibility in terms of the JavaScript that can be processed. This is probably not the best idea for production code, but does make it easier to demonstrate use of various JavaScript features within Java.

The first example to use this relatively arbitrary JavaScript processing takes advantage of JavaScript's Date object. The sample code is shown next.


System.out.println(
"Today's Date: "
+ processArbitraryJavaScript(
"var date = new Date(); var month = (date.getMonth()+1).toFixed(0)",
"month",
null) + "/"
+ processArbitraryJavaScript(
"var date = new Date(); var day = date.getDate().toFixed(0)",
"day",
null) + "/"
+ processArbitraryJavaScript(
"var date = new Date(); var year = date.getFullYear().toFixed(0)",
"year",
null) );


This code specifies that a JavaScript Date should be retrieved (which will be the current date) and that month, date of month, and full year should be extracted from that instantiated Date. The output for this appears next.



The last example worked on an arbitrary JavaScript String but did not use any parameters. The next example demonstrates providing of parameters to this arbitrary JavaScript String processing as it demonstrates use of JavaScript's pow function. The code for this example is listed next.


final Map<String, Object> exponentParameters = new HashMap<String, Object>();
exponentParameters.put("base", 2);
exponentParameters.put("exponent", 5);
System.out.println(
"2 to the 5 is: "
+ processArbitraryJavaScript(
"var answer = Math.pow(base,exponent)",
"answer",
exponentParameters) );


The output from running this example is shown in the following screen snapshot.



For my final example of this blog posting, I demonstrate the standard toString() output of the ScriptException declared in some of the previous examples. The ScriptEngine.eval method throws this checked exception if there is an error in executing/evaluating the provided script. This method also throws a NullPointerException if the provided String is null. The code used to force a script error is shown next.


/**
* Intentionally cause script handling error to show the type of information
* that a ScriptException includes.
*/
public static void testScriptExceptionHandling()
{
System.out.println(processArbitraryJavaScript("Garbage In", "none", null));
}


This code provides a nonsensical script (in terms of JavaScript syntax), but that is exactly what is needed to demonstrate the ScriptException.toString(), which is called as part of the exception handling in the method shown above for handling an arbitrary JavaScript String. When the code is executed, we see the exception information as shown in the next image.



The portion of the output that comes from ScriptException.toString() is the portion that states: "javax.script.ScriptException: sun.org.mozilla.javascript.internal.EvaluatorException: missing ; before statement (<Unknown source>#1) in <Unknown source> at line number 1."

The ScriptException contains the file name, line number, and column number of the exception, which is especially helpful if a file with JavaScript code is provided for evaluation.


Conclusion

Java SE 6 makes it simple to use JavaScript within Java code. Other scripting engines can also be associated with Java, but it is handy to have one provided out-of-the-box with Mozilla Rhino.


Complete Code and Output Screen Snapshot

For completeness, I am including the complete code listing in one place here and the resultant output after that.

JavaScriptInJavaExample.java


package dustin.examples;

import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

/**
* This example demonstrates using JavaScript within Java SE 6.
*/
public class JavaScriptInJavaExample
{
private final static ScriptEngineManager manager = new ScriptEngineManager();

/** Using java.util.logging. */
private final static Logger LOGGER = Logger.getLogger(
JavaScriptInJavaExample.class.getName());

private final static String NEW_LINE = System.getProperty("line.separator");

private final static String HEADER_SEPARATOR =
"=======================================================================";

/**
* Write the key information about the provided Script Engine Factories to
* the provided OutputStream.
*
* @param scriptEngineFactories Script Engine Factories for which key data
* should be written to the OutputStream.
* @param out OutputStream to which to write Script Engine Factory details.
*/
public static void writeScriptEngineFactoriesToOutputStream(
final List<ScriptEngineFactory> scriptEngineFactories,
final OutputStream out)
{
printHeader("Available Script Engines", out);
try
{
out.write(NEW_LINE.getBytes());
for (final ScriptEngineFactory scriptEngine : scriptEngineFactories)
{
out.write(
( scriptEngine.getEngineName() + " ("
+ scriptEngine.getEngineVersion() + ")" + NEW_LINE).getBytes());
out.write(
( "\tLanguage: " + scriptEngine.getLanguageName() + "("
+ scriptEngine.getLanguageVersion() + ")" + NEW_LINE).getBytes());
out.write("\tCommon Names/Aliases: ".getBytes());
for (final String engineAlias : scriptEngine.getNames())
{
out.write((engineAlias + " ").getBytes());
}
out.write(NEW_LINE.getBytes());
}
out.write(NEW_LINE.getBytes());
}
catch (IOException ioEx)
{
LOGGER.severe(
"Could not write to provided OutputStream: "
+ ioEx.toString());
}
}

/**
* Show availability of scripting engines supported in this environment.
*/
public static void testSupportedScriptingEngines()
{
writeScriptEngineFactoriesToOutputStream(
manager.getEngineFactories(), System.out);
}

/**
* Write number in exponential form.
*
* @param numberToWriteInExponentialForm The number to be represented in
* exponential form.
* @param numberDecimalPlaces The number of decimal places to be used in the
* exponential representation.
* @param out OutputStream to which exponential number should be written.
*/
public static void writeNumberAsExponential(
final Number numberToWriteInExponentialForm,
final int numberDecimalPlaces,
final OutputStream out)
{
final ScriptEngine engine = manager.getEngineByName("js");
try
{
engine.put("inputNumber", numberToWriteInExponentialForm);
engine.put("decimalPlaces", numberDecimalPlaces);
engine.eval("var outputNumber = inputNumber.toExponential(decimalPlaces);");
final String exponentialNumber = (String) engine.get("outputNumber");
out.write(("Number: " + exponentialNumber + NEW_LINE).getBytes());
}
catch (ScriptException scriptException)
{
LOGGER.severe(
"ScriptException encountered trying to write exponential: "
+ scriptException.toString());
}
catch (IOException ioEx)
{
LOGGER.severe(
"IOException encountered trying to write exponential: "
+ ioEx.toString());
}
}

/**
* Test JavaScript within Java.
*/
public static void testJavaScriptInJava()
{
printHeader("Writing Numbers as Exponentials", System.out);
final int sourceNumber = 675456;
writeNumberAsExponential(sourceNumber, 1, System.out);
writeNumberAsExponential(sourceNumber, 2, System.out);
writeNumberAsExponential(sourceNumber, 3, System.out);
writeNumberAsExponential(sourceNumber, 4, System.out);
writeNumberAsExponential(sourceNumber, 5, System.out);
}

/**
* Process the passed-in JavaScript script that should include an assignment
* to a variable with the name prescribed by the provided nameOfOutput and
* may include parameters prescribed by inputParameters.
*
* @param javaScriptCodeToProcess The String containing JavaScript code to
* be evaluated. This String is not checked for any type of validity and
* might possibly lead to the throwing of a ScriptException, which would
* be logged.
* @param nameOfOutput The name of the output variable associated with the
* provided JavaScript script.
* @param inputParameters Optional map of parameter names to parameter values
* that might be employed in the provided JavaScript script. This map
* may be null if no input parameters are expected in the script.
*/
public static Object processArbitraryJavaScript(
final String javaScriptCodeToProcess,
final String nameOfOutput,
final Map<String, Object> inputParameters)
{
Object result = null;
final ScriptEngine engine = manager.getEngineByName("js");
try
{
if (inputParameters != null)
{
for (final Map.Entry<String,Object> parameter :
inputParameters.entrySet())
{
engine.put(parameter.getKey(), parameter.getValue());
}
}
engine.eval(javaScriptCodeToProcess);
result = engine.get(nameOfOutput);
}
catch (ScriptException scriptException)
{
LOGGER.severe(
"ScriptException encountered trying to write arbitrary JavaScript '"
+ javaScriptCodeToProcess + "': "
+ scriptException.toString());
}
return result;
}

/**
* Write passed-in headerMessage text to provided OutputStream using clear
* header demarcation.
*
* @param headerMessage Text to be written to header.
* @param out OutputStream to which header should be written.
*/
private static void printHeader(
final String headerMessage, final OutputStream out)
{
try
{
out.write((NEW_LINE + HEADER_SEPARATOR + NEW_LINE).getBytes());
out.write((headerMessage + NEW_LINE).getBytes());
out.write((HEADER_SEPARATOR + NEW_LINE).getBytes());
}
catch (IOException ioEx)
{
LOGGER.warning(
"Not able to write header with text '"
+ headerMessage
+ " out to provided OutputStream: " + ioEx.toString());
System.out.println(HEADER_SEPARATOR);
System.out.println(headerMessage);
System.out.println(HEADER_SEPARATOR);
}
}

/**
* Demonstrate execution of an arbitrary JavaScript script within Java that
* does NOT include parameters.
*/
public static void testArbitraryJavaScriptStringEvaluationWithoutParameters()
{
printHeader(
"Use JavaScript's Date Object [script with NO parameters]", System.out);
System.out.println(
NEW_LINE + "Today's Date: "
+ processArbitraryJavaScript(
"var date = new Date(); var month = (date.getMonth()+1).toFixed(0)",
"month",
null) + "/"
+ processArbitraryJavaScript(
"var date = new Date(); var day = date.getDate().toFixed(0)",
"day",
null) + "/"
+ processArbitraryJavaScript(
"var date = new Date(); var year = date.getFullYear().toFixed(0)",
"year",
null)
+ NEW_LINE);
}

/**
* Demonstrate execution of an arbritrary JavaScript script within Java
* that includes parameters.
*/
public static void testArbitraryJavaScriptStringEvaluationWithParameters()
{
printHeader(
"Use JavaScript's Math.pow(base,exponent) function [script WITH parameters]",
System.out);
final Map<String, Object> exponentParameters = new HashMap<String, Object>();
exponentParameters.put("base", 2);
exponentParameters.put("exponent", 5);
System.out.println(
"2 to the 5 is: "
+ processArbitraryJavaScript(
"var answer = Math.pow(base,exponent)",
"answer",
exponentParameters)
+ NEW_LINE);
}

/**
* Intentionally cause script handling error to show the type of information
* that a ScriptException includes.
*/
public static void testScriptExceptionHandling()
{
printHeader(
"Intentional Script Error to Demonstate ScriptException", System.out);
System.out.println(
NEW_LINE + processArbitraryJavaScript("Garbage In", "none", null));
}

/**
* Main executable for demonstrating running of script code within Java.
*/
public static void main(final String[] arguments)
{
testSupportedScriptingEngines();
testJavaScriptInJava();
testArbitraryJavaScriptStringEvaluationWithoutParameters();
testArbitraryJavaScriptStringEvaluationWithParameters();
testScriptExceptionHandling();
}
}


Output from Running Above Code Sample

6 comments:

C.W. said...

I find it intriguing that you can run arbitrary javascript from within Java, but I have yet to find a valid reason to do so... any suggested use cases?

James said...

How about automated testing of javascript generated by frameworks like GWT?

Dominique D. said...

IMHO, Java and JavaScript come closer. See my post A false piece of news may become real as Java and JS move closer. And there are already different use cases for using both (my post mentions some of them, like building core GUI components with Java, and let use JS for scripting the all).

kozmoz said...

Only with the Sun release of Java SE version 6 comes a version of Rhine JavaScript bundled. Other implementations of Java 6 may differ: for instance, the version of Java 6 supplied by Apple for Mac OS X does NOT include a JavaScript engine by default.

Dustin said...

Rob (kozmoz),

Thanks for pointing out that Rhino JavaScript engine is not necessarily delivered with all implementations of Java, though all implementations of Java do (or should) support the ability to associate scripting language engines with them via JSR 223 (Scripting for the Java Platform). I had intended to address this, but got caught up in the hurry to post the blog entry. It is beneficial to have this explicitly called out.

Dustin said...

C.W.,

Thanks for posting the question. It brings out another item I would have liked to address, but did not remember to when I actually wrote the post.

James and Dominique D. point out some good use cases (including the referenced post) and I think John O'Conner's article Scripting for the Java Platform outlines several good reasons for mixing scripting with Java in the section Reasons to Use a Scripting Language.