Monday, June 17, 2013

Taming the Nashorn...AGAIN...

Hi there,
Like I said some post ago I will make use of the Nashorn scripting capabilities more often in the future and so here is another example.
To be honest I'm looking in the Nashorn scripting stuff only because I would like to improve my workflow for the Raspberry Pi embedded development. Usually I create and test the jar files on my desktop computer before I copy them via scp to the Raspberry Pi. On the Pi I then execute them on the command line. In principle this is ok but if you would like to test new things you have to do the modification-compile-copy procedure very often and that takes time. So the idea is that as soon as a new JDK8 developer preview for the Raspberry Pi is out (and hopefully contains Nashorn) I could create a Java application that loads a JavaScript file at runtime and executes the code in the JavaScript file. With this scenario I could edit the JavaScript file with nano for example and simply start the jar again. This would improve the development workflow a lot. So because we don't have Nashorn on the Raspberry Pi at the moment I have to test it on my desktop where it is part of the JDK8 developer preview already.

Example:
Let's take a gauge from the Enzo library that will look like this...


The idea is to configure the gauge with default values if no JavaScript file is present and if there is a JavaScript file with a given name at the same place as the jar file it should be loaded and the configuration should be taken from the JavaScript file.
To realize that we first of all need a simple JavaFX application that contains the gauge.
So this is the code for that...

public class GaugeDemo extends Application {
    private Gauge gauge;

    @Override public void init() {
        gauge = new Gauge();
    }

    @Override public void start(Stage stage) {
        StackPane pane = new StackPane();
        pane.getChildren().add(gauge);

        Scene scene = new Scene(pane);

        stage.setTitle("Nashorn demo");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}

Simple...isn't it...? Ok, now let's create a method that configures the gauge with some default values. In this case we will define things like a title, a unit, some sections and modify the colors of the sections. So the code will look like this...

private void setGaugeDefaults() {
    gauge.setTitle("Default");
    gauge.setUnit("°F");
    gauge.setMinValue(32);
    gauge.setMaxValue(212);
    gauge.setSections(new Section(104, 140),
                      new Section(140, 176),
                      new Section(176, 212));
    gauge.setStyle("-needle       : rgb(  0,   0, 255);" +
                   "-section0-fill: rgb(  0, 255,   0);" +
                   "-section1-fill: rgb(255, 255,   0);" +
                   "-section2-fill: rgb(255,   0,   0);");

}

Please keep in mind that calling setStyle() is not the best way to set a JavaFX CSS style but it's ok for this example!!!

If we call this method in the init() method after we have initialized the Gauge object the result will look like this...


So we now have the default style that should be used if no JavaScript file is present. So now we need a method to load a JavaScript file. We need the JavaScript file to be loaded into a String object that we could evaluate with the JavaScript engine. The code that we need to realize that looks like follows...

private String getScript(final String fileName) {
    StringBuilder scriptContent = new StringBuilder();
    try {
        Path         path  = Paths.get(fileName);
        List<String> lines = 
            Files.readAllLines(path, StandardCharsets.UTF_8);
        for(String line : lines) {
            scriptContent.append(line);
        }        
    } catch (IOException | URISyntaxException exception) {}
    return scriptContent.toString();

}

Now we are able to load a JavaScript file into a String so it's time to initialize the JavaScript engine...means unleash the Nashorn... :)
But before we can do that we need a JavaScript file. The idea is that we could pass the gauge object to the JavaScript file and modifiy the gauge within the JavaScript file.
So the JavaScript file will look like follows...

var ArrayList = java.util.ArrayList;
var Gauge     = Packages.eu.hansolo.enzo.gauge.Gauge;
var Section   = Packages.eu.hansolo.enzo.gauge.Section;

var obj = new Object();

obj.initGauge = function(gauge) {
    gauge.title      = 'Nashorn';
    gauge.unit       = '°C';
    gauge.minValue   = 0;
    gauge.maxValue   = 100;

    var sections     = new ArrayList();
    sections.add(new Section(40, 60));
    sections.add(new Section(60, 80));
    sections.add(new Section(80, 100));
    gauge.sections   = sections;

    gauge.style      = "-needle         : rgb(243,56,28);" +
                       "-section0-fill  : rgb(192, 215, 123);" +
                       "-section1-fill  : rgb(217, 191, 78);" +
                       "-section2-fill  : rgb(225, 75, 69);"

}

So the JavaScript above will initialize the gauge for measuring temperature in degree celsius where the setGaugeDefaults() method in our Java class will initialize the gauge for measuring the temperature in degree fahrenheit. Means it should be very easy to see if the script was loaded or not.
Because we could have more than one method in our JavaScript file that we might call in our Java code I created a method that loads the script once, get's the obj Object from the script an then we can use this script object to call the methods. So here is the method that will instantiate three member variables (the ScriptEngine, the so called Invocable and an Object that contains the JavaScript obj)...

private void initScript(final String script) {
    try {
        engine.eval(script);
        inv          = (Invocable) engine;
        scriptObject = engine.get("obj");
    } catch (ScriptException exception) { 
        script = ""
    }

}

Now we only have to add a method that will initialize the gauge object in dependence on the presence of a JavaScript file. So if the JavaScript file is present it should be taken to initialize the gauge otherwise the gauge should be initialized by calling the setGaugeDefaults() method. And here is the code...

private void initGauge() {
    gauge = new Gauge();
    if (script.isEmpty()) {
        // Default Gauge settings
        setGaugeDefaults();
    } else {
        // Settings from Script
        try {
            inv.invokeMethod(scriptObject, "initGauge", gauge );
        } catch (ScriptException | NoSuchMethodException | 
            RuntimeException exception) {
            // In case of an error set default values
            setGaugeDefaults();
        }
    }

}

With this method in place we are able to initialize our gauge by loading a JavaScript file during runtime...sweet.
So the complete Java code will look similar to this...

public class GaugeDemo extends Application {
    private static final String  SCRIPT_FILE_NAME = "config.js";
    private static final Charset ENCODING         = StandardCharsets.UTF_8;
    private ScriptEngineManager  manager;
    private ScriptEngine         engine;
    private String               script;
    private Invocable            inv;
    private Object               scriptObject;
    private Gauge                gauge;


    // ******************** Initialization ************************************
    @Override public void init() {
        manager = new ScriptEngineManager();
        engine  = manager.getEngineByName("nashorn");
        script  = getScript(SCRIPT_FILE_NAME);
        initScript(script);
        initGauge();
    }

    private void initScript(final String script) {
        try {
            engine.eval(script);
            inv          = (Invocable) engine;
            scriptObject = engine.get("obj");
        } catch (ScriptException exception) { 
            script = ""
        }
    }

    private void initGauge() {
        gauge = new Gauge();
        if (script.isEmpty()) {
            // Default Gauge settings
            setGaugeDefaults();
        } else {
            // Settings from Script
            try {
                inv.invokeMethod(scriptObject, "initGauge", gauge );
            } catch (ScriptException | NoSuchMethodException | 
                RuntimeException exception) {
                System.out.println("Error executing initGauge() in JavaScript");
                setGaugeDefaults();
            }
        }
    }


    // ******************** Methods *******************************************
    private void setGaugeDefaults() {
        gauge.setTitle("Default");
        gauge.setUnit("°F");
        gauge.setMinValue(32);
        gauge.setMaxValue(212);
        gauge.setSections(new Section(104, 140),
                          new Section(140, 176),
                          new Section(176, 212));
        gauge.setStyle("-needle         : rgb(0, 150,   0);" +
                       "-section0-fill  : rgb(0, 100, 150);" +
                       "-section1-fill  : rgb(0, 100, 200);" +
                       "-section2-fill  : rgb(0, 100, 255);");
    }

    private String getScript(final String fileName) {
        StringBuilder scriptContent = new StringBuilder();
        try {

            // ******** NEEDED FOR SCRIPT IN SAME FOLDER AS JAR ***************
            final URL    root       = GaugeDemo.class.getProtectionDomain()
                                               .getCodeSource().getLocation();
            final String scriptPath =
                (new File(root.toURI())).getParentFile().getPath() + 
                File.separator + fileName;
            // ****************************************************************            

            Path         path       = Paths.get(scriptPath);
            List<String> lines      = Files.readAllLines(path, ENCODING);
            for (String line : lines) {
                scriptContent.append(line);
            }
            System.out.println("Script: " + scriptPath + " loaded");
        } catch (IOException | URISyntaxException e) {}
        return scriptContent.toString();
    }


    // ******************** Start application *********************************
    @Override public void start(Stage stage) {        
        StackPane pane = new StackPane();
        pane.setPadding(new Insets(10, 10, 10, 10));
        pane.setPrefSize(400, 400);

        pane.getChildren().addAll(gauge);

        Scene scene = new Scene(pane);

        stage.setTitle("Nashorn JavaFX");
        stage.setScene(scene);
        stage.show();        
    }

    public static void main(String[] args) {
        launch(args);
    }

}

Be aware that in the code above the JavaScript file should be placed in the same folder as the jar. If you would like to load the JavaScript with an absolute path you simply could remove the block that is marked in the code and use the full path name for the SCRIPT_FILE_NAME constant.

If I start the code above the result looks like this...


As you can see the gauge was initialized by loading and evaluating the JavaScript file "config.js".

So let's hope the Nashorn will find it's way to the JDK8 ARM developer preview very soon... :)

I hope you enjoy playing with the Nashorn as much as I do...so keep coding...

1 comment: