Script Thy Java App

Let’s imagine a very simple scenario. You are writing an application which would benefit from some user specific customization after deployment. Maybe some of your customers have specific algorithms to be executed in the context of your application. Maybe your application doesn’t solve just one problem but it is designed to solve a class of problems but the number of variants is too high for you to capture at development time.

What you would like to do is to provide an API so your customers can implement their own algorithms. But providing a Java API for your application is a huge complication for any non trivial project.
Fortunately there is another possible solution – make your application scriptable. In Java 6 standard support for scripting engines was introduced (JSR 223: Scripting for the JavaTM Platform). There are many possible applications of this technology but this tutorial will focus on a very specific scenario.

Let’s assume you decide to allow your customers to write extensions for your application in any scripting language they like as long as it is supported on Java 6. These scripting languages have to provide a script engine that implements JSR 223 Scripting APIs.

To make this possible you have to provide a way for your application to run custom scripts in any language. Also you have to provide an application internal object model that will be accessible to the custom scripts.

The following example will use a trivial scenario that will prove the point. The object model to be exposed to the scripts is just a simple counter object (Counter.java):

package com.littletutorials.om;

public class Counter {
    private int i = 0;

    public int getI() {
        return i;
    }

    public void setI(int i) {
        this.i = i;
    }
}

All the custom scripts will have to provide an implementation of an application specific interface. In our case the scripts will be treated as plugins (Plugin.java):

package com.littletutorials.om;

public interface Plugin {
    public void execute();
}

At this point we have everything we need for our customers: an API (the object model – Counter) and a standard interface for their scripts (the Plugin interface). Internally we also need a way to load the scripts and execute them. Of course for a real application you can implement a very sophisticated architecture for script configuration, discovery, initialization and execution. But for this tutorial we are going to implement a very simple solution based on restrictions:

  • all the scripts will reside in one folder
  • only one script of each type is allowed
  • all the scripts will be called “plugin.ext” where the extension is language dependent (js, py, rb, groovy…)

The name is used to identify a plugin script and the extension to identify the scripting engine.

Here is our script plugin manager combined with our application (ScriptPluginApp.java):

package com.littletutorials.scripting;

import java.io.*;
import java.util.*;
import javax.script.*;
import com.littletutorials.om.*;

public class ScriptPluginApp {
    private static final String PLUGIN_PREFIX = "plugin";
    private static final String VAR_COUNTER = "counter";
    private static final String VAR_PLUGIN = "plugin";

    // prepare the application object model
    Counter counter = new Counter();

    // prepare plug-in repository (just a list)
    List<Plugin> plugins = new ArrayList<Plugin>();

    private List<Plugin> loadPlugins(File pluginDir)
        throws ScriptException, FileNotFoundException {

        File[] pluginScripts = pluginDir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.startsWith(PLUGIN_PREFIX) ? true : false;
            }
        });

        // create the script engine manager
        ScriptEngineManager factory = new ScriptEngineManager();

        for (File script: pluginScripts) {
            String name = script.getName();
            String ext = name.substring(name.indexOf(".") + 1, name.length());

            ScriptEngine engine = factory.getEngineByExtension(ext);
            // pass the object model
            engine.put(VAR_COUNTER, counter);
            // evaluate the plug-in script
            engine.eval(new java.io.FileReader(script.getAbsolutePath()));
            // store the plug-in
            com.littletutorials.om.Plugin p = (Plugin) engine.get(VAR_PLUGIN);
            plugins.add(p);
        }
        return plugins;
    }

    private void executePlugins() {
        for (Plugin p: plugins) {
            p.execute();
        }

        // check the effect
        System.out.println("Final counter = " + counter.getI());
    }

    public static void main(String[] args) throws Exception {

        if (args.length != 1) {
            System.out.println("Wrong number of arguments! " + 
                args.length + " present, 1 expected.");
            System.exit(1);
        }

        File pluginDir = new File(args[0]);
        if (! pluginDir.isDirectory()) {
            System.out.println("Plugin folder does not exist: " + 
                pluginDir.getAbsolutePath());
            System.exit(2);
        }

        ScriptPluginApp app = new ScriptPluginApp();
        // load plug-ins
        app.loadPlugins(pluginDir);
        // execute plug-ins
        app.executePlugins();
    }
}

Keep in mind this is just a sample application. Don’t take it as a model for exception handling. Follow the exception handling policy shown here. :)

There are two steps left:

  • download the scripting engines and the scripting language implementations
  • write the scripts as plugins in as many languages as you want

JSR 223 supported scripted can be downloaded from https://scripting.dev.java.net/ and this page also has links to the sites hosting supported language implementations. The samples in this tutorial require the default JavaScript implementation (no download needed) and also support for Groovy, Jython and JRuby.

Download everything you need, read the instructions and note all the jar files you will need to include in the class path.

Now is time to write the scripts and place them all in a “plugins” folder. Each script will implement the Plugin interface and will create an instance of that implementation. Here they are:

JavaScript: plugin.js

println("Initialize JavaScript plugin");

var plugin  = new com.littletutorials.om.Plugin() {
    execute: function() {
        i = counter.getI();
        println("JavaScript initial counter = " + i);
        counter.setI(i + 1);
    }
};

Groovy: plugin.groovy

import com.littletutorials.om.*;

class GroovyPlugin implements Plugin {

    private Counter counter;

    GroovyPlugin(Counter c) {
        counter = c;
    }

    void execute() {
        int i = counter.getI();
        println("Groovy initial counter = " + i);
        counter.setI(i + 1);
    }
}

println("Initialize Groovy plugin");
plugin = new GroovyPlugin(counter);

Jython: plugin.py

from com.littletutorials.om import *

class JythonPlugin(Plugin):
    def __init__(self, counter):
        self.counter = counter

    def execute(self):
        i = counter.getI()
        print "Jython initial counter = " + repr(i)
        counter.setI(i + 1)

print 'Initialize Jython plugin'
plugin = JythonPlugin(counter)

JRuby: plugin.rb

include_class 'com.littletutorials.om.Counter'
include_class 'com.littletutorials.om.Plugin'

class RubyPlugin
    include Plugin
    def initialize(c)
        @counter = c
    end

    def execute()
        i = @counter.getI()
        puts "JRuby initial counter = #{i}\n"
        @counter.setI(i + 1)
    end
end

puts "Initialize JRuby plugin\n"
$plugin = RubyPlugin.new($counter)

To run the application type this:

java -cp {the big class path with all the jars} com.littletutorials.scripting.ScriptPluginApp {plugins folder}

You should enjoy an output like this:

Initialize Jython plugin
Initialize Groovy plugin
Initialize JavaScript plugin
Initialize JRuby plugin
Jython initial counter = 0
Groovy initial counter = 1
JavaScript initial counter = 2
JRuby initial counter = 3
Final counter = 4

I hope this was instructive and fun. If you have a bit of time try to implement the plugin in another scripting language and maybe post it here.

For a tutorial on scripting Scala applications read:
Script Your Scala Application with JRuby, Jython, Groovy and JavaScript

2 thoughts on “Script Thy Java App”

  1. nice tutorial, thanks

    but a counter class should rather have increment() method – using setter (counter.set(i+1)) smells like rotten JavaBean, uh, ugly smell, uh…

    cheers
    Tomek

Comments are closed.