Wednesday May 29, 2013

Repost: Taming the Nashorn (first impressions)...

[This is a repost of an article written by our friend @hansolo_ with his permission.  The original article is at  Taming the Nashorn (first impressions)...]

Hi there,

JavaScript is everywhere these days and so it is also part of JDK8 dev. prev. with Project Nashorn which is a lightweight high-performance JavaScript runtime engine.

Because I never used Rhino (which is in principle the same just older and slower) I had no  idea how to use it and for what reason.

After I met Marcus Lagergren (@lagergren) at JAX in Mainz (Germany), Geecon in Krakow (Poland) and at Java Forum in Malmö (Sweden) I decided that I have to take a look at the Nashorn to get an idea on how to use it. Of course I'm especially interested on how to use Nashorn in combination with JavaFX. Lucky me that Nashorn is part of the weekly developer previews of JDK8 since a few weeks ago and because Jim Laskey (@wickund) blogged about this use case I was able to start with it yesterday.

After some starting problems I slowly begin to understand and would like to share it with you...

First of all I would like to see how I could use Nashorn with JavaScript in combination with my controls, so the idea is to visualize a Lcd control of my Enzo library with Nashorn.

For the following example you'll need the jdk8 developer preview (grab it here) and a current version of my Enzo library (grab it here).

And with a shell and a text editor you are good to go...really nice :)


So first of all we need to create a JavaScript file that should create a JavaFX application which should show the Lcd control and set's the value of the Lcd control every 3 seconds to a random value between 0 and 100.


This is the JavaFX code to realize that...


import eu.hansolo.enzo.lcd.Lcd;
import eu.hansolo.enzo.lcd.LcdBuilder;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.util.Random;
public class EnzoFX extends Application {
  private Lcd            lcd;
  private Random         random;
  private long           lastTimerCall;
  private double         charge;
  private AnimationTimer timer;
  @Override public void init() {
    // Initialize the AnimationTimer
    random        = new Random();
    lastTimerCall = System.nanoTime();
    charge        = 0;
    timer         = new AnimationTimer() {
      @Override public void handle(long now) {
        if (now > lastTimerCall + 3_000_000_000l) {
          lcd.setValue(random.nextDouble() * 100);
          lcd.setTrend(Lcd.Trend.values()[random.nextInt(5)]);
          charge += 0.02;
          if (charge > 1.0) charge = 0.0;
          lcd.setBatteryCharge(charge);
          lastTimerCall = now;
          System.out.println(lcd.getValue());
        }
      }
    };
    // Initialize the Enzo Lcd control
    lcd = LcdBuilder.create()
                    .styleClass(Lcd.STYLE_CLASS_STANDARD_GREEN)
                    .title("Room Temp")
                    .unit("°C")
                    .decimals(2)
                    .minMeasuredValueDecimals(2)
                    .maxMeasuredValueDecimals(2)
                    .unitVisible(true)
                    .batteryVisible(true)
                    .alarmVisible(true)
                    .minMeasuredValueVisible(true)
                    .maxMeasuredValueVisible(true)
                    .lowerRightTextVisible(true)
                    .formerValueVisible(true)
                    .trendVisible(true)
                    .lowerRightText("Info")
                    .valueAnimationEnabled(true)
                    .foregroundShadowVisible(true)
                    .crystalOverlayVisible(true)
                    .build();
  }
  @Override public void start(Stage stage) {
    // Prepare stage and add controls
    StackPane root = new StackPane();
    root.setPadding(new Insets(10, 10, 10, 10));
    root.getChildren().add(lcd);
    stage.setTitle("Enzo in JavaFX");
    stage.setScene(new Scene(root, 528, 192));
    stage.show();
    // Start the timer
    timer.start();
  }
  public static void main(String[] args) {
    launch(args);
  }
}

Ok, so now we know how to achieve this in JavaFX but now let's take a look at the JavaScript code that leads to the same result. Here you go...


var System         = java.lang.System;
var Random         = java.util.Random;
var StackPane      = javafx.scene.layout.StackPane;
var Scene          = javafx.scene.Scene;
var Insets         = javafx.geometry.Insets;
var AnimationTimer = javafx.animation.AnimationTimer;
var Lcd            = Packages.eu.hansolo.enzo.lcd.Lcd;
// Initialize the AnimationTimer
var random        = new Random();
var lastTimerCall = System.nanoTime();
var charge        = 0;
var timer         = new AnimationTimer() {
    handle: function(now) {
        if (now > lastTimerCall + 3000000000) {
            lcd.value = random.nextDouble() * 100;
            lcd.trend = Lcd.Trend.values()[random.nextInt(5)];
            charge += 0.02;
            if (charge > 1.0) charge = 0.0;
            lcd.batteryCharge = charge;
            lastTimerCall = now;
            print(lcd.value);
        }
    }
}
// Initialize the Enzo Lcd control
var lcd   = new Lcd();
lcd.styleClass.add(Lcd.STYLE_CLASS_STANDARD_GREEN);
lcd.title                    = "Room Temp";
lcd.unit                     = "°C";
lcd.decimals                 = 2;
lcd.minMeasuredValueDecimals = 2;
lcd.maxMeasuredValueDecimals = 2;
lcd.unitVisible              = true;
lcd.batteryVisible           = true;
lcd.alarmVisible             = true;
lcd.minMeasuredValueVisible  = true;
lcd.maxMeasuredValueVisible  = true;
lcd.lowerRightTextVisible    = true;
lcd.formerValueVisible       = true;
lcd.trendVisible             = true;
lcd.lowerRightText           = "Info";
lcd.valueAnimationEnabled    = true;
lcd.foregroundShadowVisible  = true;
lcd.crystalOverlayVisible    = true;
// Prepare the stage and add controls
var root     = new StackPane();
root.padding = new Insets(10, 10, 10, 10);
root.children.add(lcd);
$STAGE.title = "Enzo with Nashorn";
$STAGE.scene = new Scene(root, 528, 192);
$STAGE.show();
// Start the timer
timer.start();

The JavaScript code that I saved to a file "javafx.js" looks not that different from the JavaFX code except...it's JavaScript...and the resulting application will look like this...




You might ask yourself how you could run this code and here is the answer, all it takes is one call on the command line that looks like this:


/PATH/TO/JDK/jdk1.8.0.jdk/Contents/Home/jre/bin/jjs
-cp /PATH/TO/LIBRARY/Enzo.jar -fx /PATH/TO/JS-FILE/javafx.js


Means you have to now the path to your jdk8 installation folder where you could find the jjs executable that is needed to start nashorn from the command line. In addition you have to add the Enzo.jar to the classpath so that you could use it within your JavaScript file and at last you have to call the JavaScript file with the -fx parameter which will start the application....BAM...that's all...sweet :)

I've put the commandline in a bash script so that I could call it by simple calling the bash script instead of typing the whole line over and over again.


If you start the javafx.js file you should see something like on this youtube video.


Ok you might say where is the advantage of using JavaScript to run a JavaFX application...just think about not starting an IDE, not compiling and building a JavaFX application just to make a short test. Isn't it be much nicer to simply have a small JavaScript, edit it with your default text editor and run it with from the command line with one single call...I really like that for testing things.


If you would like to know more about Nashorn you should definitely take a look at the Nashorn blog ,subscribe to the Nashorn mailing list and take a look at the Nashorn Wiki.


At this point I would say THANK YOU to Jim Laskey and Marcus Lagergren which helped me to getting this stuff done!!!


This is really a first test of using Nashorn with JavaFX but now that I know how to use it I will use more often...so stay tuned and keep coding...


Tuesday May 07, 2013

jjs -fx

So after some playing around and working with the JavaFX folks, I think we have a proposal for jjs that works with JavaFX.  The -fx flag on jjs will bootstrap scripts using a javafx.application.Application.  Thus, writing JavaFX scripts in Nashorn is very easy.

The basic command line is;
	jjs -fx fxscript.js
You can mix and match other jjs options like -scripting and -- ;
	jjs -fx -scripting fxscript.js -- my script args
The content of the script follows some of the examples I've posted before.  The script may optionally contain JavaFX init, start and/or stop functions.  What is new, is that you can leave them behind and just straight script.  The original hello world example;

var Button    = Java.type("javafx.scene.control.Button");
var StackPane = Java.type("javafx.scene.layout.StackPane");
var Scene     = Java.type("javafx.scene.Scene");

function start(stage) {
    stage.title = "Hello World!";
    var button = new Button();
    button.text = "Say 'Hello World'";
    button.onAction = function() print("Hello World!");
    var root = new StackPane();
    root.children.add(button);
    stage.scene = new Scene(root, 300, 250);
    stage.show();
}

becomes;

var Button    = Java.type("javafx.scene.control.Button");
var StackPane = Java.type("javafx.scene.layout.StackPane");
var Scene     = Java.type("javafx.scene.Scene");

$STAGE.title = "Hello World!";
var button = new Button();
button.text = "Say 'Hello World'";
button.onAction = function() print("Hello World!");
var root = new StackPane();
root.children.add(button);
$STAGE.scene = new Scene(root, 300, 250);
$STAGE.show();

where the stage is now a global var $STAGE (instead of the start function argument.)

Also for convenience, we've predefined includes for all of the JavaFX classes.  I would recommend using only the classes you need (only needed for new and for static field access), but for prototyping having includes really helps things move along.

The hello world example can then be rewritten as;

load("fx:base.js");
load("fx:controls.js");
load("fx:graphics.js");

$STAGE.title = "Hello World!";
var button = new Button();
button.text = "Say 'Hello World'";
button.onAction = function() print("Hello World!");
var root = new StackPane();
root.children.add(button);
$STAGE.scene = new Scene(root, 300, 250);
$STAGE.show();

The complete set of includes are as follows;

fx:base.js
	javafx.stage.Stage
	javafx.scene.Scene
	javafx.scene.Group
	javafx/beans
	javafx/collections
	javafx/events
	javafx/util

fx:graphics.js
	javafx/animation
	javafx/application
	javafx/concurrent
	javafx/css
	javafx/geometry
	javafx/print
	javafx/scene
	javafx/stage

fx:controls.js
	javafx/scene/chart
	javafx/scene/control

fx:fxml.js
	javafx/fxml

fx:web.js
	javafx/scene/web

fx:media.js
	javafx/scene/media

fx:swing.js
	javafx/embed/swing

fx:swt.js
	javafx/embed/swt

Here are a couple more examples;

// fx3d.js

load("fx:base.js");
load("fx:controls.js");
load("fx:graphics.js");
 
var material = new PhongMaterial();
material.diffuseColor = Color.LIGHTGREEN;
material.specularColor = Color.rgb(30, 30, 30);

var meshView = Java.to([
    new Box(200, 200, 200),
    new Sphere(100),
    new Cylinder(100, 200)
], "javafx.scene.shape.Shape3D[]");

for (var i = 0; i != 3; i++) {
    meshView[i].material = material;
    meshView[i].translateX = (i + 1) * 220;
    meshView[i].translateY = 500;
    meshView[i].translateZ = 20;
    meshView[i].drawMode = DrawMode.FILL;
    meshView[i].cullFace = CullFace.BACK;
};

var pointLight = new PointLight(Color.WHITE);
pointLight.translateX = 800;
pointLight.translateY = -200;
pointLight.translateZ = -1000;

var root = new Group(meshView);
root.children.add(pointLight);

var scene = new Scene(root, 800, 800, true);
scene.fill = Color.rgb(127, 127, 127);
scene.camera = new PerspectiveCamera(false);
$STAGE.scene = scene;
$STAGE.show();

// ColorfulCircles.js

load("fx:base.js");
load("fx:controls.js");
load("fx:graphics.js");

var WIDTH = 500;
var HEIGHT = 600;
var animation;

function setup(primaryStage) {
    var root = new Group();
    primaryStage.resizable = false;
    var scene = new Scene(root, WIDTH, HEIGHT);
    scene.title = "Colourful Circles";
    primaryStage.scene = scene;
    
    // create first list of circles
    var layer1 = new Group();
    for(var i = 0; i < 15; i++) {
        var circle = new Circle(200, Color.web("white", 0.05));
        circle.strokeType = StrokeType.OUTSIDE;
        circle.stroke = Color.web("white", 0.2);
        circle.strokeWidth = 4;
        layer1.children.add(circle);
    }
    
    // create second list of circles
    var layer2 = new Group();
    for(var i = 0; i < 20; i++) {
        var circle = new Circle(70, Color.web("white", 0.05));
        circle.strokeType = StrokeType.OUTSIDE;
        circle.stroke = Color.web("white", 0.1);
        circle.strokeWidth = 2;
        layer2.children.add(circle);
    }
    
    // create third list of circles
    var layer3 = new Group();
    for(var i = 0; i < 10; i++) {
        var circle = new Circle(150, Color.web("white", 0.05));
        circle.strokeType = StrokeType.OUTSIDE;
        circle.stroke = Color.web("white", 0.16);
        circle.strokeWidth = 4;
        layer3.children.add(circle);
    }
    
    // Set a blur effect on each layer
    layer1.effect = new BoxBlur(30, 30, 3);
    layer2.effect = new BoxBlur(2, 2, 2);
    layer3.effect = new BoxBlur(10, 10, 3);
    
    // create a rectangle size of window with colored gradient
    var colors = new Rectangle(WIDTH, HEIGHT,
            new LinearGradient(0, 1, 1, 0, true, CycleMethod.NO_CYCLE,
                               new Stop(0,    Color.web("#f8bd55")),
                               new Stop(0.14, Color.web("#c0fe56")),
                               new Stop(0.28, Color.web("#5dfbc1")),
                               new Stop(0.43, Color.web("#64c2f8")),
                               new Stop(0.57, Color.web("#be4af7")),
                               new Stop(0.71, Color.web("#ed5fc2")),
                               new Stop(0.85, Color.web("#ef504c")),
                               new Stop(1,    Color.web("#f2660f"))));
    colors.blendMode = BlendMode.OVERLAY;
    
    // create main content
    var group = new Group(new Rectangle(WIDTH, HEIGHT, Color.BLACK),
                          layer1, 
                          layer2,
                          layer3,
                          colors);
    var clip = new Rectangle(WIDTH, HEIGHT);
    clip.smooth = false;
    group.clip = clip;
    root.children.add(group);
    
    // create list of all circles
    var allCircles = new java.util.ArrayList();
    allCircles.addAll(layer1.children);
    allCircles.addAll(layer2.children);
    allCircles.addAll(layer3.children);
    
    // Create a animation to randomly move every circle in allCircles
    animation = new Timeline();
    for each (var circle in allCircles) {
        animation.getKeyFrames().addAll(
              new KeyFrame(Duration.ZERO, // set start position at 0s
                           new KeyValue(circle.translateXProperty(), Math.random() * WIDTH),
                           new KeyValue(circle.translateYProperty(), Math.random() * HEIGHT)),
              new KeyFrame(new Duration(20000), // set end position at 20s
                           new KeyValue(circle.translateXProperty(), Math.random() * WIDTH),
                           new KeyValue(circle.translateYProperty(), Math.random() * HEIGHT))
              );
    }
    animation.autoReverse = true;
    animation.cycleCount = Animation.INDEFINITE;
}

function stop() {
    animation.stop();
}

function play() {
    animation.play();
}

function start(primaryStage) {
    setup(primaryStage);
    primaryStage.show();
    play();
}



Playing ITunes from Nashorn

If you are on Mac OS X, it's possible to use Apple Script from Nashorn.  For example if you want to play a song in iTunes, the following script will do it;

#!/usr/bin/jjs -scripting
#
var song = "Bring It On Home";

$EXEC("/usr/bin/osascript", <<<EOD);
tell application "iTunes"
	activate
	play track "${song}" in playlist 1
end tell
EOD

HTTP Server Written In Nashorn

This is just a little "thinking outside the box" example.


Node.js is hot and anyone who has used it knows there is a ton of applications possible. Some of Node.js features remind me of a tool kit I had, when I worked for another server company prior to Oracle. All my development was remote and had to run on remote servers. I hate typing and ssh/console was my only way in. So, I decided that I would create a little HTTP server on the remote end, to proxy all my routine repository, build and file editing tasks via a browser. This didn't take a lot of effort and was very flexible.


There are many many HTTP server apps out there (many written in Java.) I could modify one of them, but I just wanted to prove to myself it could all be done in pure Nashorn. This is what I whipped up;


#!/usr/bin/jjs -scripting
#

var Thread            = java.lang.Thread;
var ServerSocket      = java.net.ServerSocket;
var PrintWriter       = java.io.PrintWriter;
var InputStreamReader = java.io.InputStreamReader;
var BufferedReader    = java.io.BufferedReader;
var FileInputStream   = java.io.FileInputStream;
var ByteArray         = Java.type("byte[]");

var PORT = 8080;
var CRLF = "\r\n";
var FOUROHFOUR = <<<EOD;
<HTML>
    <HEAD>
        <TITLE>404 Not Found</TITLE>
    </HEAD>
    <BODY>
        <P>404 Not Found</P>
    </BODY>
</HTML>
EOD

var serverSocket = new ServerSocket(PORT);

while (true) {
    var socket = serverSocket.accept();
    
    try {
        var thread = new Thread(function() { httpRequestHandler(socket); });
        thread.start();
        Thread.sleep(100);
    } catch (e) {
        print(e);
    }
}

function httpRequestHandler(socket) {
    var out       = socket.getOutputStream();
    var output    = new PrintWriter(out);
    var inReader  = new InputStreamReader(socket.getInputStream(), 'utf-8');
    var bufReader = new BufferedReader(inReader);
    
    var lines = readLines(bufReader);
    
    if (lines.length > 0) {
        var header = lines[0].split(/\b\s+/);

        if (header[0] == "GET") {
            var URI = header[1].split(/\?/);
            var path = String("./serverpages" + URI[0]);
    
            try {
                if (path.endsWith(".jjsp")) {
                    var body = load(path);
                    if (!body) throw "JJSP failed";
                    respond(output, "HTTP/1.0 200 OK", "text/html", body);
                } else {
                    sendFile(output, out, path);
                }
            } catch (e) {
                respond(output, "HTTP/1.0 404 Not Found", "text/html", FOUROHFOUR);
            }
        }
    }
    
    output.flush();
    bufReader.close();
    socket.close();
}

function respond(output, status, type, body) {
    sendBytes(output, status + CRLF);
    sendBytes(output, "Server: Simple Nashorn HTTP Server" + CRLF);
    sendBytes(output, "Content-type: ${type}" + CRLF);
    sendBytes(output, "Content-Length: ${body.length}" + CRLF);
    sendBytes(output, CRLF);
    sendBytes(output, body);
}

function contentType(path) {
    if (path.endsWith(".htm") ||
        path.endsWith(".html")) {
      return "text/html";
    } else if (path.endsWith(".txt")) {
      return "text/text";
    } else if (path.endsWith(".jpg") ||
               path.endsWith(".jpeg")) {
      return "image/jpeg";
    } else if (path.endsWith(".gif")) {
      return "image/gif";
    } else {
      return "application/octet-stream";
    }
}

function readLines(bufReader) {
    var lines = [];
    
    try {
        var line;
        while (line = bufReader.readLine()) {
            lines.push(line);
        }
    } catch (e) {
    }
    
    return lines;
}

function sendBytes(output, line) {
    output.write(String(line));
}

function sendFile(output, out, path) {
    var file = new FileInputStream(path);

    var type = contentType(path);
    sendBytes(output, "HTTP/1.0 200 OK" + CRLF);
    sendBytes(output, "Server: Simple Nashorn HTTP Server" + CRLF);
    sendBytes(output, "Content-type: ${contentType(path)}" + CRLF);
    sendBytes(output, "Content-Length: ${file.available()}" + CRLF);
    sendBytes(output, CRLF);
    output.flush();
    
    var buffer = new ByteArray(1024);
    var bytes = 0;
    
    while ((bytes = file.read(buffer)) != -1) {
        out.write(buffer, 0, bytes);
    }
}

Short and sweet, all done in 84 lines of JavaScript. I could have handled more of the http spec, but this will do as POC.


You can use this server to download html, jpeg et al, but in the midst of this server code you will see the following lines;


                if (path.endsWith(".jjsp")) {
                    var body = load(path);
                    if (!body) throw "JJSP failed";
                    respond(output, "HTTP/1.0 200 OK", "text/html", body);

This reads as, if the requested file ends with .jjsp, then evaluate the content as JavaScript and use the final result as an HTML response.


The following is a sample ".jjsp" file the I used for testing.


#!/usr/bin/jjs -scripting
#

var colours = {
    java: "BLUE",
    js: "RED", 
    css: "GREEN",
    html: "ORANGE"
};

function colorize(file) {
    var suffix = file.substr(file.lastIndexOf(".") + 1);
    var colour = colours[suffix];
    if (colour) {
        return "<FONT COLOR='${colour}'>${file}</FONT>";
    }
    return file;
}

var files = `ls`.trim().split("\n");
files = files.map(colorize);
files = files.map(function(file) "${file}<BR />");
files = files.join("\n");

var HTML = <<<EOD;
<HTML>
    <HEAD>
        <TITLE>Simple HTML</TITLE>
    </HEAD>
    <BODY>
        <img width="256" height="138" src="rrr256x138.jpg" alt="rrr256x138" />
        <BR />
        <FONT FACE="Courier New" SIZE="2">
        ${files}
        </FONT>
    </BODY>
</HTML>
EOD

HTML;

Note that both the JavaScript and HTML content of the .jjsp file is very easy to read.  This particular script catalogs a directory (ls) and colourizes the result depending on the file extension. In my case the result was;

FX3D.js
HTML.js
HelloWorld.java
Server
Server.zip
SimpleHTTPServer.js
Test.class
Test.java
Text.js
canvas.js
colourfulcircles.js
hellofxml.js
helloworld1.js
helloworld2.js
http.js
introspect.js
jogl
languages.css
languages1.js
languages2.js
script.js
scripting.fxml
serverpages
slow1.js
suspect.js
test.js
threading.js
trends.css
trends.js
twitter4j-core-3.0.1.jar
twitter4j.properties

Lots of possibilities.

About

Technical discussions and status of the Nashorn JavaScript Project.

Search

Categories
Archives
« May 2013 »
SunMonTueWedThuFriSat
   
1
2
3
4
5
6
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
30
31
 
       
Today