terça-feira, 15 de abril de 2014

Writing JavaFX apps using Javascript with the Java 8 Nashorn engine

Java 8 was just a great release! We already talked about Lambdas and created JavaFX 8 apps on this blog. Bruno Borges created a fx2048, a JavaFX version of the famous 2048 game, which shows Lambdas and the Stream library usage.

Today I'll rewrite the Sentiments App view in Javascript using the new JS engine named Nashorn, one of the exciting Java 8 new features.

Loading the Script


We load a file with the view contents, but before doing this, we add the main Stage so it can be referenced from the javascript file. That's enough for the Java part!

package org.jugvale.sentiments.view;

import javafx.application.Application;
import javafx.stage.Stage;
import javax.script.*;
import java.io.FileReader;

public class AppJS extends Application{

        private final String SCRIPT = getClass().getResource("/sentimentsView.js").getPath();

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

        @Override
        public void start(Stage stage){
                try{
                        ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
                        engine.put("stage", stage);
                        engine.eval(new FileReader(SCRIPT));
                }catch(Exception e){ 
                        e.printStackTrace();
                }   
        }   
 }



Notice in my case I'm calling the script from my Java code, but it's possible to use jjs to invoke scripts and build JavaFX apps.

Writing Javascript

After this, we can already start writing Javascritp code and leave Java behind for a moment. The core of the application (the service and model classes)  is written in Java and we can refer to it in our JS.

The good parts

The best thing about JS I can highlight is that we don't need to use get/set methods anymore! Nashorn threat Java beans properties (those accessed by get and set methods) as an object property and behind the scenes it will invoke the get/set methods!
We also take full advantage of the scripting language: don't need to use semiclon, easier to declare variables, more clar
I liked the fact that I don't need to recompile the class to update the view. If I modify the script and re-run the java part, the view will be updated!

The bad parts

I didn't like how to handle the imports. It seems that I have to either declare the type or write the FQN of a class to instantiate it.
I also prefer Java and FXML over writing the view in pure javascript. Remember you can script on FXML as well!

Code and running

Here's the final script for the Sentiments App view:

// Imports
var Scene = Java.type('javafx.scene.Scene')
var VBox = Java.type('javafx.scene.layout.VBox')
var Label = Java.type('javafx.scene.control.Label')
var TextField = Java.type('javafx.scene.control.TextField')
var PieChart = Java.type('javafx.scene.chart.PieChart')
var TableView  = Java.type('javafx.scene.control.TableView')
var TableColumn  = Java.type('javafx.scene.control.TableColumn')
var PropertyValueFactory  = Java.type('javafx.scene.control.cell.PropertyValueFactory')
var SearchService  = Java.type('org.jugvale.sentiments.service.SearchService')
var TextSentimentService  = Java.type('org.jugvale.sentiments.service.TextSentimentService')
var TableView  = Java.type('javafx.scene.control.TableView')
var FXCollections = Java.type('javafx.collections.FXCollections')

// variable declaration
var root = new VBox(10)
var txtQuery = new TextField()
var table = new TableView()
var chart = new PieChart()
var textSentimentService = new TextSentimentService()
var searchService = new SearchService()

// main block
root.children.addAll(txtQuery, chart, table)
stage.scene = new Scene(root)
stage.title = "Sentiments"
stage.width = 400
stage.height = 550
stage.show()
initializeApp()
// functions 
function initializeApp(){
 txtQuery.promptText = "Enter your query for text and press enter"
 chart.title = "Sentiments for \"" + txtQuery.getText() +"\""
 txtQuery.onAction = queryAction
 addTableColumns()
 queryAction(null)
}
function queryAction(e){
 var textSentiments = getTextSentiments(txtQuery.getText());
 fillTable(textSentiments);
 fillChart(textSentiments);
}
function fillTable(textSentiments){
 table.items = FXCollections.observableList(textSentiments)
}
function fillChart(textSentiments){
 var polarity0Count = textSentiments.stream().filter(function (s) s.getPolarity() == 0).count();
 var polarity2Count = textSentiments.stream().filter(function(s)  s.getPolarity() == 2).count();
 var polarity4Count = textSentiments.stream().filter(function(s)  s.getPolarity() == 4).count();
 chart.data = 
  FXCollections.observableArrayList(
   new PieChart.Data(":(", polarity0Count),
   new PieChart.Data(":|", polarity2Count),
   new PieChart.Data(":)", polarity4Count)
 )
}
function getTextSentiments(q){
 return textSentimentService.getSentiments(query(q)).textSentiments
}
function addTableColumns(){
 var textCol = new TableColumn("Text")
 var polarityCol = new TableColumn("Polarity")
 textCol.cellValueFactory = new PropertyValueFactory("text")
 textCol.minWidth = 200
 polarityCol.cellValueFactory = new PropertyValueFactory("polarity")
 polarityCol.minWidth = 80
 polarityCol.resizable = false
 table.columns.setAll(textCol, polarityCol)
}
function query(q){
 if(!q || q.trim().isEmpty()){
  return java.util.Arrays.asList("obama is awesome", "obama sucks", "obama eats potato");
 }else{
  return  searchService.search(q, SearchService.Provider.TWITTER);
 }
}


The full code is on github and you can build it by simply typing the following maven instruction on command line (remember to have maven and JAVA_HOME pointing to your Java 8 installation root directory):

$ mvn package exec:java -Dexec.mainClass="org.jugvale.sentiments.view.AppJS" -DskipTests

Conclusion

Using Javascript from Java is easy and with the new JS engine we have increased the performance due the Invoke Dynamic feature.
Using JS can be a good advantage for a team who has JS programmers and needs to build a desktop app, so we can use their full potential on Javascript and take all advantages and libraries from Java.

terça-feira, 8 de abril de 2014

Having fun creating a JavaFX App to Visualize Tweet Sentiments

A cool think that people loves to do is to create apps that analyze the sentiment of some text stream, such as Twitter. To better understand, see a graphical sentiment analysis for the Java 8 release messages on Twitter (source):



In this post, I'm going to create a simple, really simple, app that queries twitter, and then access the Sentiment140 REST API to analyze the tweet's content sentiment. After this, I'll count the results in a chart and also list the text content in a table.

Using the Sentiment140 REST API

The API is really simple to use. Since I'm lazy, I love simplicity. To use it, submit a text and it will return a simple JSON with a number value called polarity, which possible values are:
  • 0: negative
  • 2: neutral
  • 4: positive
The API documentation contains all the information you need to use it. Again, I simply loved how easy is to use it. For example, if I want to submit the text "I Love REST APIs" to the API, I would have to HTTP POST a JSON containing it to the URL http://www.sentiment140.com/api/bulkClassifyJson  I would use the following curl command:


$ curl -d "{'data': [{'text': 'I Love REST APIs'}]}" http://www.sentiment140.com/api/bulkClassifyJson

And it will return:

{"data":[{"text":"I Love REST APIs","polarity":4,"meta":{"language":"en"}}]}

 See, it's not hard!

Querying Twitter

I wanted to query twitter, it was my plan... However, it's "new" API requires me to use OAuth to query. Twitter4j API, however, is great to handle Twitter, so I decided to keep twitter... See the code I use to query twitter:

private List searchTwitter(String q){ 
        Twitter twitter = new TwitterFactory().getInstance();
        twitter.setOAuthConsumer(TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_KEY_SECRET);
        Query twitterQuery = new Query("lang:en " + q); 
        try{
                return twitter.search(twitterQuery)
                        .getTweets().stream()
                        // map to text and replace all special chars
                        .map(s -> s.getText().replaceAll("[^a-zA-Z0-9\\s\\,\\.\\']+",""))
                        .peek(System.out::println)
                        .collect(Collectors.toList());
        }catch(Exception e){ 
                throw new RuntimeException(e);
        }   
}

Accessing the APIs with Java

The first thing to do is to use model what we want in a class and what is want is basically what the Sentiment140 API returns. So we created a Java bean named TextSentiment which code is below:

@JsonIgnoreProperties(ignoreUnknown = true)
public class TextSentiment{
        private String text;
        private int polarity;

         // getters setters and constructors
}

@XmlRootElement
public class TextSentimentList{
        @XmlElement(name="data")
        private List textSentiments;
         // getters setters 
}


Now, we have need to find a good way to send HTTP requests to this API and return instances of TextSentiment. To do this We created a class called TextSentimentService, and in this class we make use of JAX-RS 2 Client API to query Sentiment140, so we don't waste time parsing data... The method getSentiment140 was used to get the sentiment of all the Strings we passed

import org.jugvale.sentiments.model.TextSentimentList;
import org.jugvale.sentiments.model.TextSentiment;
import java.util.List;
import java.util.stream.Collectors;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;

public class TextSentimentService{

        private final String SENTIMENTS_BASE_URL = "http://www.sentiment140.com/api/bulkClassifyJson";
        private final String MEDIA_TYPE = "application/json";

        public TextSentimentList getSentiments(List texts){
                TextSentimentList requestEntity = new TextSentimentList();
                requestEntity.setTextSentiments(
                        texts.stream().map(TextSentiment::new).collect(Collectors.toList())
                );  
                return ClientBuilder.newClient()
                                .target(SENTIMENTS_BASE_URL)
                                .request(MEDIA_TYPE)
                                .buildPost(Entity.entity(requestEntity, MEDIA_TYPE))
                                .invoke(TextSentimentList.class);
        }   

}



To access Twitter, we created a class SearchService, which has a method called search. In this method, we specify the provider we are wanting to retrieve texts to analyze, in our case, we have twitter:

import java.util.List;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import twitter4j.*;
import java.util.stream.Collectors;
public class SearchService{

        /*
         *It's mine!!! Please, use yours
         * */

        private final static String TWITTER_CONSUMER_KEY = "{KEY}";
        private final static String TWITTER_CONSUMER_KEY_SECRET = "{SECRET}";

        public static enum Provider{
                TWITTER("Twitter");

                private String name;
                Provider(String name){
                        this.name = name;
                }   
                public String toString(){
                        return this.name;
                }   
        }   

        public List search(String q, Provider p){ 
                switch (p){
                        case TWITTER:
                                return searchTwitter(q);
                        default:
                                throw new IllegalArgumentException("Provider \"" + p + "\" not implemented");
                }   
        }  
        // searchTwitter method...
}



Notice that both Service classes can be extended to make use of another sentiment API or another social media API.

Testing our service classes

Before continuing with our application, let's make a simple test of the Sentiment Service API, see the code below:

import org.junit.Test;
import org.jugvale.sentiments.service.*;
import org.jugvale.sentiments.model.*;
import java.util.Arrays;

public class TestService{

        @Test
        public void testServices(){
                String TXT_1 = "obama is awesome";
                String TXT_2 = "obama sucks";
                String TXT_3 = "obama is eating a potato";
                TextSentimentService s = new TextSentimentService();
                TextSentimentList list = s.getSentiments(Arrays.asList(TXT_1, TXT_2, TXT_3));
                list.getTextSentiments().stream().forEach(System.out::println);
                //TODO: Create a Map with key equal the text and polarity is the value
        }   
}


With this small and simple test, we make sure we are accessing the API and returning something, so we also confirm the parsing of the response if correct.

The JavaFX APP

 Our small APP contains one text field, one chart and one table! The user enters the query on the TextField, press enter, then our app takes that query to search the social media, get the results and then it sends the texts to the Sentiment140 API analyze. With the results, we update the user interface. See the code:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.scene.control.*;
import javafx.scene.control.cell.*;
import javafx.scene.layout.VBox;
import javafx.scene.chart.*;
import org.jugvale.sentiments.service.*;
import org.jugvale.sentiments.model.*;
import java.util.*;

public class App extends Application{

 final TextSentimentService textSentimentService = new TextSentimentService();
 final SearchService searchService = new SearchService();
 final PieChart chart =  new PieChart();
 final TableView table = new TableView<>();
 final TextField txtQuery = new TextField();

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

 @Override
 public void start(Stage stage){
  VBox root = new VBox(10);
  root.getChildren().addAll(txtQuery, chart, table);
  stage.setScene(new Scene(root));
  stage.setWidth(400);
  stage.setHeight(550);
  stage.setTitle("Sentiments");
  stage.show();
  txtQuery.setOnAction(e -> {
   List textSentiments = getTextSentiments(txtQuery.getText());
   fillTable(textSentiments);
   fillChart(textSentiments);
   chart.setTitle("Sentiments for \"" + txtQuery.getText() +"\"");
  });
  txtQuery.setPromptText("Enter your query for text and press enter");
  initializeApp();  
 }

 private void initializeApp(){
  txtQuery.setText(null);
  txtQuery.getOnAction().handle(null);
  addTableColumns();
 }
 
 private void addTableColumns(){
  TableColumn textCol = new TableColumn<>("Text");
  textCol.setCellValueFactory(new PropertyValueFactory("text"));
  textCol.setMinWidth(200);
  TableColumn polarityCol = new TableColumn<>("Polarity");
  polarityCol.setCellValueFactory(new PropertyValueFactory("polarity"));
  polarityCol.setMinWidth(80);
  polarityCol.setResizable(false);
  table.getColumns().setAll(textCol, polarityCol);
 }

 public void fillTable(List textSentiments){
  table.setItems(FXCollections.observableArrayList(textSentiments));
 }

 public void fillChart(List textSentiments){
  long polarity0Count = textSentiments.stream().filter(s -> s.getPolarity() == 0).count();
  long polarity2Count = textSentiments.stream().filter(s -> s.getPolarity() == 2).count();
  long polarity4Count = textSentiments.stream().filter(s -> s.getPolarity() == 4).count();
  chart.setData(
   FXCollections.observableArrayList(
    new PieChart.Data(":(", polarity0Count),
    new PieChart.Data(":|", polarity2Count),
    new PieChart.Data(":)", polarity4Count)
  ));
 }
 public List getTextSentiments(String q){
  return textSentimentService.getSentiments(query(q)).getTextSentiments();
 }

 public List query(String q){
  if(q == null){
   return Arrays.asList("obama is awesome", "obama sucks", "obama eats potato");
  }else{
   // TODO: add new providers and adapt the app to support it
   return  searchService.search(q, SearchService.Provider.TWITTER);
  }
 }
 }


Notice we use Java 8 features to deal with the List of  TextSentiment objects! Also, we used Lambda in some places of the code, can you spot it?

Do you wanna play with it?


The app code is on github and it uses Maven. if you want to play with it, use git app to clone the repository locally and build it. Remember to use Java 8 and Maven 3.x!  Steps:

 Clone the APP

$ git clone https://github.com/jesuino/sentiments-app.git
 
Go to the app root directory  and use the following command to build:

$ cd sentiments-app
$ mvn clean package

You can also run a test:

$ mvn test

Finally, you can run the App using the run.sh script. Remember to set JAVA_HOME correctly! run.sh content:

export JAVA_HOME=/opt/java/jdk1.8.0/
mvn package exec:java  -Dexec.mainClass="org.jugvale.sentiments.view.App" -DskipTests



App screenshots

Searching for happy words:




Searching for sad words:


Conclusion

 That's all, folks. Just wanted to share this small fun app with you. I made the code really simple because I'm lazy to someone extend it. If you extend it, please, send me pull requests :)
  • Extending by adding other text sentiment analyzer. Lots of text sentiments APIs here.
  • Extending by adding other source then twitter, for example:  news sites, google plus, facebook etc.
  • Improve the view by adding more controls, a "waiting screen", etc
  • Add a CSS to improve the look of the app



quinta-feira, 20 de março de 2014

4 Real World Lambdas with Collections examples

I love functional programming and I'm exciting not only about Lambda, but also about the stream API  and the new functional interfaces that comes with Java 8, which was released recently. In the next lines I'll show some codes I took from some Java projects in github and rewrote it using Java 8. Notice obviously I had to mock some classes just to focus on the code rewrite.

If you find a better way to do this, please let me know!

Code on Github



1) Concatenating a List into a String


Original


public static String matrixparamtest(PathSegment id){
         StringBuffer sb = new StringBuffer();
         sb.append("matrix=");
         sb.append("/" + id.getPath());
         Map matrix = id.getMatrixParameters();
         Set keys = matrix.keySet();
         for (Object key : keys)
         {   
            sb.append(";" + key.toString() + "=" +
                    matrix.get(key.toString()));

         }   
         return sb.toString();
} 

My Java 8 Equivalent:


public static String matrixparamtestlambda(PathSegment id){
        Map matrix = id.getMatrixParameters();
        String keys = matrix.keySet().stream()
                    .map(k -> k + "=" + matrix.get(k))
                    .collect(Collectors.joining(";", ";", ""));
        return "matrix=/" + id.getPath() + keys;

}

What's going on here?

Take the Stream from the keys: matrix.keySet().stream()
Map to a Stream with format "param=val":  map(k -> k + "=" + matrix.get(k)
Join the stream into a big string starting with ";" and separated by ";": collect(Collectors.joining(";", ";", ""))


2) Mapping not null elements from a List

Original

public static Map mapElements(){
        final Map map = new HashMap();
        for (final Element element : values()) {
                final String name = element.getLocalName();
                if (name != null)
                        map.put(name, element);
        }   
        return map;  
}    

My Java 8 Equivalent:

public static Map mapElementsLambda(){
        return values().stream()
                .filter(e -> e.getLocalName() != null)
                .collect(Collectors.toMap(Element::getLocalName, e -> e));
}

What's going on here?

Taking the stream object from values: values().stream()
Filtering them to avoid null values: filter(e -> e.getLocalName() != null)
Collecting a map using as the key the localName attribute and as value the element itself: collect(Collectors.toMap(Element::getLocalName, e -> e));

3) Transforming a Map

Original

public static Map filterLoadedValues(Map in) {   
        Map out = new HashMap<>();
        for (Iterator iterator = in.entrySet().iterator(); iterator.hasNext();){   
                Map.Entry entry = (Map.Entry) iterator.next();
                out.put(entry.getKey().toString(), Float.valueOf(entry.getValue().toString()));
        }   
        return out;
}

My Java 8 Equivalent:

public static Map filterLoadedValuesLambda(Map in) {   
        return in.entrySet().stream().collect(
                        Collectors.toMap(
                                e -> e.getKey().toString(),
                                e -> Float.valueOf(e.getValue().toString())    
                )); 
} 

What's going on here?


  • We are taking a Stream of the entries of the map: in.entrySet().stream()
  • Collecting the entries: collect
  • Generating a new one a transforming the value from String to Float: Collectors.toMap( e -> e.getKey().toString(),e -> Float.valueOf(e.getValue().toString()) ;

4) Summing

Original

public static float getTotalPrice() {
        float totalPrice = 0;
        if (lineItems != null) {
                for (LineItem item : lineItems) {
                        if (item.getPrice() != null)
                                totalPrice += item.getPrice();
                }   
        }   
        return totalPrice;
}  

My Java 8 Equivalent:

public static double getTotalPriceLambda() {
        return lineItems.stream().mapToDouble(LineItem::getPrice).filter(Objects::nonNull).sum();
} 

What's going on here?

  • Map the stream to a DoubleStream: mapToDouble(LineItem::getPrice)
  • Filter the Stream and remove possible null values: filter(Object::nonNull)
  • Sum all the elements: sum();

Conclusion

Java 8 brings lots of advantages to programming when dealing with lists. As you can see, I took code from "real world" and not sample applications to demonstrate Lambda. It shows that Lambda brings productivity to our daily code.


sexta-feira, 21 de fevereiro de 2014

A minimal Java 8 project using Maven

To use Java 8 with Maven is easy. Maven already supports it and it's good for JavaFX developers that a Java 8 is a JavaFX 8 application already!
To make easy the creation of new projects using JavaFX 8, I created an archetype for JavaFX applications. It will be useful for me, so it might be useful for someone else as well.

Using Maven with JavaFX 8

Having Maven 3.x and a Java 8 newest build, you are able to create JavaFX 8 applications using Maven! I created a "Hello World" application to serve as a starting point for new applications. The pom.xml is on github and the directories structure is as follow:

Building and Running from Maven

Make sure you export JAVA_HOME to the root path of your Java 8 installation. In my case, before running Maven I used the followin command (in Ubuntu 12.04):

export JAVA_HOME=/opt/java/jdk1.8.0/ 

After this you should be able to build your project:

$ mvn clean project

Notice that in the root directory you will have a jar, it's the final application! You should be able to run it using java:

$ java -jar target/jfx8-app.jar

To run the Java application main method using maven, we basically have three ways. To run this project, I used the following maven command: 

$ mvn exec:java -Dexec.mainClass="org.jugvale.App"

Then you should be able to see the application, which a really simple one. See the code(notice it's using Lambdas!!!) and an App screenshot:
package org.jugvale;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;

public class App extends Application{

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

        @Override
        public void start(Stage stage){
                Button btn = new Button(">> Click <<");
                btn.setOnAction(e -> System.out.println("Hello JavaFX 8"));
                StackPane root = new StackPane();
                root.getChildren().add(btn);
                stage.setScene(new Scene(root));
                stage.setWidth(300);
                stage.setHeight(300);
                stage.setTitle("JavaFX 8 app");
                stage.show();
        }

 }



What's so nice about it?

Maven allows me to use the IDE I want(actually in this case we can't use Eclipse since it's not supporting Java 8 yet) and we can extend this archetype to, for example, add support to Android using plugins. Read more about JavaFX and Android. From this project we can also create complex application without depending on an IDE, just sit and start programming!

Using Gradle

Tobias Schulte made a version of this using Gradle. For more information see the repository he created on github.
You don't even need to download anything, just clone the project, go to its root directory and type the following command:
$ ./gradlew build
It will build the project. To run it, use the following:
$./gradlew run

* Remember to set JAVA_HOME as your JDK 8 home directory before running these commands