j2js - Java to Javascript using GWT, without the UI

After a foray into generating Javascript from Python, I decided to take a look at Google's Web Toolkit (GWT). GWT is very much oriented around building a UI in Java and deploying it into a web page. In this snippet, I look at how to generate Javascript from non-UI Java code using GWT. The resulting Javascript can be used as a library that other web applications can call. It turned out to be a bit awkward, but possible.

Setting up

First, you'll need an installation of GWT, plus the Java J2SE SDK and Apache Ant. I tested using GWT 1.6.4.

Then download j2js.zip and unzip it in the "samples" directory of your GWT installation. I've only tested this on linux, it may or may not work on other platforms without tweaking.

You should now have a j2js subdirectory under your GWT samples directory, containing the following files that we will use to get GWT to generate Javascript for a simple Java program. j2js/build.xml j2js/wordcount.html j2js/src/j2js.gwt.xml j2js/src/client/WordCountEntryPoint.java j2js/src/code/net/mccarroll/WordCount.java j2js/tools/postprocess.py

The distribution also contains two Javascript files that are generated from the java using GWT.

j2js/war/j2js/j2js.js j2js/war/j2js/j2js.nocache.js

j2js.nocache.js is the Javascript that GWT generates, with extras to allow the Javascript to by dynamically loaded by a web-page. j2js.js is extracted from this file, with unnecessary stuff filtered out, so that it can be statically included in a web page and called directly by other Javascript code.

WordCount, a simple Java example to convert

We use a simple java class, net.mccarroll.WordCount, to convert to Javascript.

WordCount.java
package net.mccarroll;

public class WordCount  {	

	public static int count(String args[]) {
		int total = 0;
		for(String arg:args) {
			total += arg.split(" ").length;
		}
		return total;
	}

}

WordCountEntryPoint, defining the API

We introduce a second java class, client.WordCountEntryPoint, which defines the API that we wish to expose to other Javascript programs.

WordCountEntryPoint.java
package client;

import net.mccarroll.WordCount;
import com.google.gwt.core.client.EntryPoint;

class WordCountEntryPoint implements EntryPoint  {	

	private static int count(String args[]) {
		return WordCount.count(args);
	}

	native void registerWrapper() /*-{
		wordcount = @client.WordCountEntryPoint::count([Ljava/lang/String;);
	}-*/;

	public void onModuleLoad() { 
		registerWrapper();
	}
}

The registerWrapper function is declared as being native (its body is defined using javascript). This uses a GWT feature called JSNI (JavaScript Native Interface) to create a global javascript variable wordcount pointing to the javascript that GWT will compile for the WordCountEWntryPoint static function count. For more information on JSNI and on this technique, see Bruce Johnson's excellent blog entry JSNI Introduction

j2js.gwt.xml, defining our GWT module

Heres the contents of file j2js.gwt.xml, which defines our GWT module. All GWT modules need this housekeeping stuff.

j2js.gwt.xml
<module>
        <inherits name="com.google.gwt.core.Core" />
	<source path="client" />
	<super-source path="code" />
	<entry-point class="client.WordCountEntryPoint" />
	<add-linker name="sso" />
</module>

There are a couple of things that are non-standard about this file.

postprocess.py

The output from GWT, j2js/war/j2js/j2js.nocache.js, has all kinds of extra stuff to enable the javascript functionality to by dynamically loaded. Its quite difficult to follow the logic, in fact. The python postprocess.py script attempts to strip out the unnecessary stuff. Whats left is written to j2js/war/j2js/j2js.js.

postprocess.py
#
# postprocess.py <modulename>
#
# extract useful javascript from the output of google web toolkit compiler 
# run using the sso linker
#
# extract a section from the <modulename>-nocache.js file (passed on stdin) 
# and send to stdout.  The extracted section is standalone javascript that
# can be included directly into web pages.
#
#
import sys

mod_name = sys.argv[1]

startmarker = mod_name + "()"

lines = sys.stdin.readlines()[:-1]
started = False
nr = 0
while nr < len(lines):
	if started:
		print lines[nr].strip("\n")
	elif lines[nr].startswith(startmarker):
		started = True
		nr += 1
		if nr < len(lines) and lines[nr].startswith("(function () {"):
			print lines[nr][14:].strip()
	nr += 1

build.xml, organising the compilation process

The ant makefile, build.xml, pulls everything together into a build process (including the invocation of the postprocess.py script).

build.xml
<?xml version="1.0" encoding="utf-8" ?>
<project name="j2js" default="build" basedir=".">

  <property name="gwt.sdk" location="../.." />
  <property name="module_name" location="j2js" />

  <path id="project.class.path">
    <pathelement location="war/WEB-INF/classes"/>
    <pathelement location="${gwt.sdk}/gwt-user.jar"/>
    <fileset dir="${gwt.sdk}" includes="gwt-dev*.jar"/>
  </path> 

  <target name="javac" depends="" description="Compile java source">
    <mkdir dir="war/WEB-INF/classes"/>
    <javac srcdir="src" includes="**" encoding="utf-8"
        destdir="war/WEB-INF/classes"
        source="1.5" target="1.5" nowarn="true"
        debug="true" debuglevel="lines,vars,source">
      <classpath refid="project.class.path"/>
    </javac>
  </target>

  <target name="gwtc" depends="javac" description="GWT compile to JavaScript">

    <java failonerror="true" fork="true" classname="com.google.gwt.dev.Compiler">
      <classpath>
        <pathelement location="src"/>
        <path refid="project.class.path"/>
      </classpath>
      <jvmarg value="-Xmx256M"/>
      <arg value="-style"/>
      <arg value="DETAILED"/>
      <arg value="-logLevel"/>
      <arg value="INFO"/>
      <arg value="j2js"/>
    </java>
  </target>

  <target name="postprocess" depends="gwtc" description="postprocess javascript" >
	<exec input="war/j2js/j2js.nocache.js" output="war/j2js/j2js.js" dir="war/j2js" executable="python" failonerror="yes">
		<arg value="../../tools/postprocess.py" />
		<arg value="j2js" />
	</exec>
  </target>

  <target name="build" depends="postprocess" description="Build this project" />

  <target name="clean" description="Cleans this project">
    <delete dir="war" failonerror="false" />
  </target>

</project>

Testing the result

The end product is the following javascript code (j2js/war/j2js/j2js.js). We can call it from a test page.

j2js.js
var $gwt_version = "1.6.4";var $wnd = window;var $doc = $wnd.document;var $moduleName, $moduleBase;var $stats = $wnd.__gwtStatsEvent ? function(a) {$wnd.__gwtStatsEvent(a)} : null;var $intern_1 = '', $intern_2 = ' ', $intern_8 = 'String;', $intern_7 = '[Ljava.lang.', $intern_6 = 'client.WordCountEntryPoint', $intern_0 = 'g', $intern_4 = 'moduleStartup', $intern_5 = 'onModuleLoadStart', $intern_3 = 'startup';
var _;
function java_lang_Object(){
}

_ = java_lang_Object.prototype = {};
_.java_lang_Object_typeId$ = 1;
function client_WordCountEntryPoint_count___3Ljava_lang_String_2(args){
  return net_mccarroll_WordCount_count___3Ljava_lang_String_2(args);
}

function com_google_gwt_lang_Array_createFromSeed__II(seedType, length){
  var array = new Array(length);
  if (seedType > 0) {
    var value = [null, 0, false, [0, 0]][seedType];
    for (var i = 0; i < length; ++i) {
      array[i] = value;
    }
  }
  return array;
}

function com_google_gwt_lang_Array_initDim__Ljava_lang_Class_2IIII(arrayClass, typeId, queryId, length, seedType){
  var result;
  result = com_google_gwt_lang_Array_createFromSeed__II(seedType, length);
  com_google_gwt_lang_Array$ExpandoWrapper_$clinit__();
  com_google_gwt_lang_Array$ExpandoWrapper_wrapArray__Lcom_google_gwt_lang_Array_2Ljava_lang_Object_2Ljava_lang_Object_2(result, com_google_gwt_lang_Array$ExpandoWrapper_expandoNames, com_google_gwt_lang_Array$ExpandoWrapper_expandoValues);
  result.java_lang_Object_typeId$ = typeId;
  return result;
}

function com_google_gwt_lang_Array(){
}

_ = com_google_gwt_lang_Array.prototype = new java_lang_Object();
_.java_lang_Object_typeId$ = 0;
_.length = 0;
function com_google_gwt_lang_Array$ExpandoWrapper_$clinit__(){
  com_google_gwt_lang_Array$ExpandoWrapper_$clinit__ = nullMethod;
  com_google_gwt_lang_Array$ExpandoWrapper_expandoNames = [];
  com_google_gwt_lang_Array$ExpandoWrapper_expandoValues = [];
  com_google_gwt_lang_Array$ExpandoWrapper_initExpandos__Lcom_google_gwt_lang_Array_2Ljava_lang_Object_2Ljava_lang_Object_2(new com_google_gwt_lang_Array(), com_google_gwt_lang_Array$ExpandoWrapper_expandoNames, com_google_gwt_lang_Array$ExpandoWrapper_expandoValues);
}

function com_google_gwt_lang_Array$ExpandoWrapper_initExpandos__Lcom_google_gwt_lang_Array_2Ljava_lang_Object_2Ljava_lang_Object_2(protoType, expandoNames, expandoValues){
  var i = 0, value;
  for (var name in protoType) {
    if (value = protoType[name]) {
      expandoNames[i] = name;
      expandoValues[i] = value;
      ++i;
    }
  }
}

function com_google_gwt_lang_Array$ExpandoWrapper_wrapArray__Lcom_google_gwt_lang_Array_2Ljava_lang_Object_2Ljava_lang_Object_2(array, expandoNames, expandoValues){
  com_google_gwt_lang_Array$ExpandoWrapper_$clinit__();
  for (var i = 0, c = expandoNames.length; i < c; ++i) {
    array[expandoNames[i]] = expandoValues[i];
  }
}

var com_google_gwt_lang_Array$ExpandoWrapper_expandoNames, com_google_gwt_lang_Array$ExpandoWrapper_expandoValues;
function java_lang_Class_createForArray__Ljava_lang_String_2Ljava_lang_String_2(packageName, className){
  var clazz;
  clazz = new java_lang_Class();
  return clazz;
}

function java_lang_Class(){
}

_ = java_lang_Class.prototype = new java_lang_Object();
_.java_lang_Object_typeId$ = 0;
function java_lang_String_$split__Ljava_lang_String_2Ljava_lang_String_2I(this$static, regex, maxMatch){
  var compiled = new RegExp(regex, $intern_0);
  var out = [];
  var count = 0;
  var trail = this$static;
  var lastTrail = null;
  while (true) {
    var matchObj = compiled.exec(trail);
    if (matchObj == null || (trail == $intern_1 || count == maxMatch - 1 && maxMatch > 0)) {
      out[count] = trail;
      break;
    }
     else {
      out[count] = trail.substring(0, matchObj.index);
      trail = trail.substring(matchObj.index + matchObj[0].length, trail.length);
      compiled.lastIndex = 0;
      if (lastTrail == trail) {
        out[count] = trail.substring(0, 1);
        trail = trail.substring(1);
      }
      lastTrail = trail;
      count++;
    }
  }
  if (maxMatch == 0) {
    var lastNonEmpty = out.length;
    while (lastNonEmpty > 0 && out[lastNonEmpty - 1] == $intern_1) {
      --lastNonEmpty;
    }
    if (lastNonEmpty < out.length) {
      out.splice(lastNonEmpty, out.length - lastNonEmpty);
    }
  }
  var jr = com_google_gwt_lang_Array_initDim__Ljava_lang_Class_2IIII(com_google_gwt_lang_ClassLiteralHolder__13Ljava_1lang_1String_12_1classLit, 0, 1, out.length, 0);
  for (var i = 0; i < out.length; ++i) {
    jr[i] = out[i];
  }
  return jr;
}

_ = String.prototype;
_.java_lang_Object_typeId$ = 2;
function net_mccarroll_WordCount_count___3Ljava_lang_String_2(args){
  var arg, arg$array, arg$index, arg$max, total;
  total = 0;
  for (arg$array = args , arg$index = 0 , arg$max = arg$array.length; arg$index < arg$max; ++arg$index) {
    arg = arg$array[arg$index];
    total += java_lang_String_$split__Ljava_lang_String_2Ljava_lang_String_2I(arg, $intern_2, 0).length;
  }
  return total;
}

function init(){
  !!$stats && $stats({moduleName:$moduleName, subSystem:$intern_3, evtGroup:$intern_4, millis:(new Date()).getTime(), type:$intern_5, className:$intern_6});
  wordcount = client_WordCountEntryPoint_count___3Ljava_lang_String_2;
}

function gwtOnLoad(errFn, modName, modBase){
  $moduleName = modName;
  $moduleBase = modBase;
  if (errFn)
    try {
      init();
    }
     catch (e) {
      errFn(modName);
    }
   else {
    init();
  }
}

function nullMethod(){
}

var com_google_gwt_lang_ClassLiteralHolder__13Ljava_1lang_1String_12_1classLit = java_lang_Class_createForArray__Ljava_lang_String_2Ljava_lang_String_2($intern_7, $intern_8);

More Information

Anyone interested in using GWT to create Javascript libraries from Java code should look at Ray Cromwell's GWT Exporter project.

 
Anti-spam check
Leave a comment