C H A P T E R  20

image

JavaFX on the Web

JavaFX provides new capabilities to interoperate with HTML5. The underlying web page–rendering engine in JavaFX is the popular open-source API called Webkit. Webkit is also used in Google’s Chrome and Apple’s Safari browsers. HTML5 is the new standard markup language for rendering content in web browsers. HTML5 content consists of JavaScript, CSS, Scalable Vector Graphics (SVG), and new HTML element tags.

The relationship between JavaFX and HTML5 is important because they complement one another by drawing from each of their individual strengths. For instance, JavaFX’s rich client APIs coupled with HTML5’s rich web content create a user experience resembling a web application with the characteristics of desktop software. This new breed of applications is called RIAs.

In this chapter, we will cover the following:

  • Embedding JavaFX applications in an HTML web page
  • Displaying HTML 5 content
  • Manipulating HTML5 content with Java code
  • Responding to HTML events
  • Displaying content from the database

20-1. Embedding JavaFX Applications in a Web Page

Problem

You hope to get promoted out of your cubicle into an office with windows by impressing your boss by creating a proof of concepts using JavaFX with your existing web development skills.

Solution

Create a Hello World application using the NetBeans IDE 7.1 or later by using its new project wizard to create an application to run in a browser. Shown following are steps to follow to create a Hello World JavaFX application that is embedded in an HTML web page:

images Note For in-depth JavaFX deployment strategies refer to Oracle’s deploying JavaFX Applications: http://download.oracle.com/javafx/2.0/deployment/deployment_toolkit.htm.

Here are the steps to follow in running the new project wizard:

  1. Select New Project in the File menu of the NetBeans IDE version 7.1 or later. Figure 20-1 highlights the menu option in the NetBeans File menu.
    images

    Figure 20-1. Creating a new JavaFX project

  2. Select JavaFX in the Categories section under Choose Project, as shown in Figure 20-2. Next, select JavaFX Application under Projects. Then click Next to proceed.
    images

    Figure 20-2. New Project dialog box

  3. Create a project by specifying a name and selecting the check box to allow the wizard to generate a main class called MyJavaFXApp.java. Figure 20-3 shows a New JavaFX application wizard that specifies the project name and location. When you finish, click the Finish button.
    images

    Figure 20-3. New JavaFX Application dialog box, in which you specify Project Name and Project Location

  4. Once your new project has been created, you modify project properties. To modify the properties, right-click the project and select Properties via the popup menu. Figure 20-4 shows the project created with a main JavaFX file named MyJavaFXApp.java.
    images

    Figure 20-4. MyJavaFXApp.java project

  5. Go into the project’s properties, as shown in Figure 20-5. Select Sources in the categories option area. Next, check the Source/Binary Format option to point to JDK 7.
    images

    Figure 20-5. Project Properties―MyJavaFXApp dialog window

  6. Select the Run option in the Categories list shown in Figure 20-6. Select the in Browser radio button option. Then click the OK button.
    images

    Figure 20-6. Setting up the Run option in Browser

  7. Run and test the project by clicking the Run button on the toolbar or the F6 key. Figure 20-7 depicts the resulting Hello World application running in a browser.
    images

    Figure 20-7. The MyJavaFXApp Hello World application running inside a browser

How It Works

To create an embedded JavaFX application inside an HTML page, you use the NetBeans IDE. Although there are different deployment strategies, such as Webstart and Standalone modes, here you use the NetBeans new project wizard to automatically deploy as a local web page containing your JavaFX application in your browser. For in-depth JavaFX deployment strategies, refer to Oracle’s Deploying JavaFX Applications: http://download.oracle.com/javafx/2.0/deployment/deployment_toolkit.htms.

Following is the code generated by this solution. You will notice the JavaFX classes being used; for example, Stage, Group, and Scene classes.

images Note You can drag the imports and body of code from another code file for this recipe into the body of your new main project class, changing the name on the class definition line, as appropriate.

Following is the source code when the NetBeans’ wizard generates a new project to create a JavaFX application embedded in a HTML web page:

package myjavafxapp;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;

/**
 *
 * @author cdea
*/
public class MyJavaFXApp extends Application {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Hello World");
        Group root = new Group();
        Scene scene = new Scene(root, 300, 250);
        Button btn = new Button();
        btn.setLayoutX(100);
        btn.setLayoutY(80);
        btn.setText("Hello World");
        btn.setOnAction(new EventHandler<ActionEvent>() {

            public void handle(ActionEvent event) {
                System.out.println("Hello World");
            }
        });
        root.getChildren().add(btn);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

In Step 1, you initiate a new project (shown in Figure 20-7). In Step 2, you select the standard JavaFX application to be created. After selecting the project type, you will be specifying the name of the project. Make sure you click the Create Application Class check box to allow the wizard to generate the MyJavaFXApp Java file. Once you have clicked Finish, your newly created application will appear in the projects tab. Next, you will take Step 5 in changing project properties.

In Step 5 you will be changing two categories: Sources and Run. In the Sources category, make sure the Source/Binary Format is set to JDK 1.6 or later. After updating the Sources category, you will be changing how the project will run (Step 6) through the Run category. In Step 6, after selecting the in Browser radio button option, you will notice the Width and Height below the working directory field. To use your own custom web page, you click the browse button to select an existing HTML file, but in this recipe you can leave it blank to allow the wizard to generate a generic HTML page. Assuming that you are done with your settings, click OK to close the Project Properties dialog window.

Last, you will run your embedded JavaFX web application (Step 7). To run your application you will want to make sure this project is set as the main project by selecting in the menu Run -> Set Main Project ->MyJavaFXApp. Once you are initiating a run, your browser will launch, containing a generic web page with your JavaFX application. You’ll also notice that a convenient link allows you to launch the application as a Webstart application (not embedded).

20-2. Displaying HTML5 Content

Problem

You are so engrossed with a project for work that you often miss your kid’s soccer games. What you need is a clock application to keep track of the time.

Solution

Create a JavaFX based-application containing an analog clock that was created as HTML5 content. Use JavaFX’s WebView API to render HTML5 content in your application.

The following source code is a JavaFX application displaying an animated analog clock. The application will load an SVG file named clock3.svg and display the contents onto the JavaFX Scene graph:

package org.java7recipes.chapter20.recipe20_02;
import java.net.URL;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

/**
 *
 * @author cdea
*/
public class DisplayHtml5Content extends Application {
    private Scene scene;
    @Override public void start(Stage stage) {
        // create the scene
        stage.setTitle("Chapter 20-2 Display Html5 Content");
        final WebView browser = new WebView();
        URL url = getClass().getResource("clock3.svg");
        browser.getEngine().load(url.toExternalForm());
        scene = new Scene(browser,590,400, Color.rgb(0, 0, 0, .80));
        stage.setScene(scene);
        stage.show();
    }

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

This JavaFX code will load and render HTML5 content. Assuming that you have a designer who has provided content such as HTML5, it will be your job to render assets in JavaFX. The following code represents an SVG file named clock3.svg that is predominantly generated by the powerful tool Inkscape, which is an illustrator tool capable of generating SVG. In the following code, notice hand-coded JavaScript code (inside the CDATA tag) that will position the second, minute, and hour hands of the clock based on the current time of day. Because all the logic (from setting the time to animating the hands) is inside this file, things are self contained, which means any HTML5 capable viewer can display the file’s contents. So when debugging, you can easily render content in any HTML5-compliant browser. Later in this chapter, we will demonstrate JavaFX code that can interact with HTML5 content.

Shown here is a pared-down version of the SVG analog clock. (To obtain the file’s source code, download the code from the book’s web site.) This is anSVG analog clock created in Inkscape (clock3.svg):

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:xlink="http://www.w3.org/1999/xlink"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="300"
   height="250"
   id="svg4171"
   version="1.1"
   inkscape:version="0.48.1 "
   sodipodi:docname="clock3.svg" onload="updateTime()">

<script>

<![CDATA[
var xmlns="http://www.w3.org/2000/svg"

function updateTime()
{
  var date = new Date()

  var hr = parseInt(date.getHours())
  if (hr > 12) {
    hr = hr - 12;
  }
  var min = parseInt(date.getMinutes())
  var sec = parseInt(date.getSeconds())
  var pi=180

  var secondAngle = sec * 6 + pi
  var minuteAngle = ( min + sec / 60 ) * 6 + pi
  var hourAngle   = (hr + min / 60 + sec /3600) * 30 + pi

  moveHands(secondAngle, minuteAngle, hourAngle)
}

function moveHands(secondAngle, minuteAngle, hourAngle) {

  var secondHand = document.getElementById("secondHand")
  var minuteHand = document.getElementById("minuteHand")
  var hourHand = document.getElementById("hourHand")

  secondHand.setAttribute("transform","rotate("+ secondAngle + ")")
  minuteHand.setAttribute("transform","rotate("+ minuteAngle +")")
  hourHand.setAttribute("transform","rotate("+ hourAngle + ")")

}

]]>

</script>
<defs id="defs4173">
... // beginning of SVG code
... // Main clock code

<g id="hands" transform="translate(108,100)">
<g id="minuteHand">
<line stroke-width="3.59497285" y2="50" stroke-linecap="round" stroke="#00fff6" opacity=".9" />
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="60min" by="360" />
</g>

<g id="hourHand">
<line stroke-width="5" y2="30" stroke-linecap="round" stroke="#ffcb00" opacity=".9" />
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="12h" by="360" />
</g>
<g id="secondHand">
<line stroke-width="2" y1="-20" y2="70" stroke-linecap="round" stroke="red"/>
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="60s" by="360" />
</g>
</g>

    ... // The rest of the Clock code: shiney glare, black button cover (center) on top of  arms

</svg>

Figure 20-8 depicts a JavaFX application, rendering the SVG file clock3.svg displaying an analog clock.

images

Figure 20-8. Analog clock

How It Works

In this recipe, you will be creating an analog clock application that will take existing HTML5 content to be rendered onto the JavaFX Scene graph. HTML5 allows the use of SVG content to be shown in browsers. SVG is similar to JavaFX’s Scene graph, in which nodes can be scaled at different sizes while preserving details. To manipulate SVG or any HTML5 elements, you will be using the JavaScript language. Depicted in Figure 20-8 is a JavaFX application displaying an animated analog clock. To learn more about SVG, visit http://www.w3schools.com/svg/default.asp. Before running this example, make sure the clock3.svg file is located in the build path. In NetBeans you may need to perform a clean and build before running the application that will copy the resource (clock3.svg) to the build path. You may also want to manually copy the clock3.svg file to reside in the build path co-located where the DisplayHtml5Content.class file is located if you are running application on the command line.

In software development you will undoubtedly experience working with a designer where he/she will use popular tools to generate web content that will be wired up to an application’s functions. To create an analog clock, I enlisted my daughter, who is quite proficient with the open-source tool Inkscape. Although Inkscape was used to generate the content for this recipe, I will not go into details regarding the tool because it is beyond the scope of this book. To learn more about Inkscape, please visit http://www.inkscape.org for tutorials and demos. To model the Designer and Developer Workflow, she created a cool looking clock and I added JavaScript/SVG code to move the clock’s hour, minute, and second hands. Inkscape allows you to create shapes, text, and effects to generate amazing illustrations. Because SVG files are considered as HTML5 content, you will be able to display SVG drawings inside of an HTML5-capable browser. In this scenario, you will be displaying the analog clock in JavaFX’s WebView node. You can think of a WebView node as a mini browser capable of loading URLs to be displayed. When loading a URL you will notice the call to getEngine().load() where the getEngine() method will return an instance of javafx.scene.web.WebEngine object. So, the WebView object is implicitly creating one javafx.scene.web.WebEngine object instance per WebView object. Shown here is the JavaFX’s WebEngine object loading a file clock3.svg:

   final WebView browser = new WebView();
   URL url = getClass().getResource("clock3.svg");
   browser.getEngine().load(url.toExternalForm());

You are probably wondering why the JavaFX source code is so small. The code is small because its job is to instantiate an instance of a javafx.scene.web.WebView that instantiates a javafx.scene.web.WebEngine class and passes a URL. After that, the WebEngine object does all the work by rendering HTML5 content just like any browser. When rendering the content, notice that the clock’s arms move or animate; for example, the second hand rotates clockwise. Before animating the clock, you have to set the clock’s initial position by calling the JavaScript updateTime() function via the onload attribute on the entire SVG document (located on the root svg element). Once the clock’s arms are set, you will add SVG code to draw and animate by using the line and animate transform elements, respectively. Shown here is a SVG code snippet to animate the second hand indefinitely:

   <g id="secondHand">
   <line stroke-width="2" y1="-20" y2="70" stroke-linecap="round" stroke="red"/>
   <animateTransform attributeName="transform" type="rotate" repeatCount="indefinite"
   dur="60s" by="360" />
   </g>

On a final note, if you want to create a clock like the one depicted in this recipe, visit http://screencasters.heathenx.org/blog to learn about all things Inkscape. Another impressive and beautiful display of custom controls that focuses on gauges and dials is the Steel Series by Gerrit Grunwald. To be totally amazed, visit his blog at http://harmoniccode.blogspot.com.

20-3. Manipulating HTML5 Content with Java Code

Problem

You are an underpaid developer, and your boss refuses to let you relocate to the cube next to the window. You must find a way to determine the weather without leaving your workspace.

Solution

Create a weather application that fetches data from Yahoo’s weather service. The following code implements a weather application that retrieves Yahoo’s weather information to be rendered as HTML in a JavaFX application:

package org.java7recipes.chapter20.recipe20_03;

import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.concurrent.Worker.State;
import javafx.scene.*;
import javafx.scene.web.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.w3c.dom.*;

/**
 * Shows a preview of the weather and 3 day forecast
 * @author cdea
 */
public class ManipulatingHtmlContent extends Application {
    String url = "http://weather.yahooapis.com/forecastrss?p=USMD0033&u=f";
    int refreshCountdown = 60;

    @Override public void start(Stage stage) {
        // create the scene
        stage.setTitle("Chapter 20-3 Manipulating HTML content");
        Group root = new Group();
        Scene scene = new Scene(root, 460, 340);

        final WebEngine webEngine = new WebEngine(url);

        StringBuilder template = new StringBuilder();
        template.append("<head> ");
        template.append("<style type="text/css">body {background-color:#b4c8ee;}</style> ");
        template.append("</head> ");
        template.append("<body id='weather_background'>");

        final String fullHtml = template.toString();

        final WebView webView = new WebView();

        IntegerProperty countDown = new SimpleIntegerProperty(refreshCountdown);
        countDown.addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue){
               // when change occurs on countDown call JavaScript to update text in HTMLwebView.getEngine().executeScript("document.getElementById('countdown').innerHTML = 'Seconds till refresh: " + newValue + "'");
                if (newValue.intValue() == 0) {
                    webEngine.reload();
                }
            }
        });
        final Timeline timeToRefresh = new Timeline();
        timeToRefresh.getKeyFrames().addAll(
                new KeyFrame(Duration.ZERO, new KeyValue(countDown, refreshCountdown)),
                new KeyFrame(Duration.seconds(refreshCountdown), new KeyValue(countDown, 0))
        );

        webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
            @Override
            public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue){
                System.out.println("done!" + newValue.toString());
                if (newValue != State.SUCCEEDED) {
                    return;
                }
                // request 200 OK
                Weather weather = parse(webEngine.getDocument());

StringBuilder locationText = new StringBuilder();
                locationText.append("<b>")
                        .append(weather.city)
                        .append(", ")
                        .append(weather.region)
                        .append(" ")
                        .append(weather.country)
                        .append("</b><br /> ");

                String timeOfWeatherTextDiv = "<b id="timeOfWeatherText">" + weather.dateTimeStr + "</b><br /> ";
                String countdownText = "<b id="countdown"></b><br /> ";
                webView.getEngine().loadContent(fullHtml + locationText.toString() +
                        timeOfWeatherTextDiv +
                        countdownText +
                        weather.htmlDescription);
                System.out.println(fullHtml + locationText.toString() +
                        timeOfWeatherTextDiv +
                        countdownText +
                        weather.htmlDescription);
                timeToRefresh.playFromStart();
            }
        });

        root.getChildren().addAll(webView);

        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args){
        Application.launch(args);
    }
    private static String obtainAttribute(NodeList nodeList, String attribute) {
        String attr = nodeList
                .item(0)
                .getAttributes()
                .getNamedItem(attribute)
                .getNodeValue()
                .toString();
        return attr;

    }
    private static Weather parse(Document doc) {

        NodeList currWeatherLocation = doc.getElementsByTagNameNS("http://xml.weather.yahoo.com/ns/rss/1.0", "location");

        Weather weather = new Weather();
        weather.city = obtainAttribute(currWeatherLocation, "city");
        weather.region = obtainAttribute(currWeatherLocation, "region");
        weather.country = obtainAttribute(currWeatherLocation, "country");

        NodeList currWeatherCondition = doc.getElementsByTagNameNS("http://xml.weather.yahoo.com/ns/rss/1.0", "condition");
        weather.dateTimeStr = obtainAttribute(currWeatherCondition, "date");
        weather.currentWeatherText = obtainAttribute(currWeatherCondition, "text");
        weather.temperature = obtainAttribute(currWeatherCondition, "temp");

        String forcast = doc.getElementsByTagName("description")
                        .item(1)
                        .getTextContent();
        weather.htmlDescription = forcast;

        return weather;
    }

}
class Weather {
    String dateTimeStr;
    String city;
    String region;
    String country;
    String currentWeatherText;
    String temperature;
    String htmlDescription;

}

Figure 20-9 depicts the weather application that fetches data from the Yahoo Weather service. In the third line of displayed text, you’ll notice that Seconds till refresh: 31 is a countdown in seconds until the next retrieval of weather information. The actual manipulation of HTML content occurs here.

images

Figure 20-9. Weather application

The following is output to the console of the HTML that is rendered onto the WebView node:

<head>
<style type="text/css">body {background-color:#b4c8ee;}
</style>
</head>
<body id='weather_background'><b>Berlin, MD US</b><br />
<b id="timeOfWeatherText">Thu, 06 Oct 2011 8:51 pm EDT</b><br />
<b id="countdown"></b><br />

<img src="http://l.yimg.com/a/i/us/we/52/33.gif"/><br />
<b>Current Conditions:</b><br />
Fair, 49 F<BR />
<BR /><b>Forecast:</b><BR />
Thu - Clear. High: 66 Low: 48<br />
Fri - Sunny. High: 71 Low: 52<br />
<br />
<a
href="http://us.rd.yahoo.com/dailynews/rss/weather/Berlin__MD/*http://weather.yahoo.com/foreca
st/USMD0033_f.html">Full Forecast at Yahoo! Weather</a><BR/><BR/>
(provided by <a href="http://www.weather.com" >The Weather Channel</a>)<br/>

How It Works

In this recipe you will be creating a JavaFX application able to retrieve XML information from Yahoo’s weather service. Once the XML is parsed, HTML content is assembled and rendered onto JavaFX’s WebView node. The WebView object instance is a graph node capable of rendering and retrieving XML or any HTML5 content. The application will also display a countdown of the number of seconds until the next retrieval from the weather service.

When accessing weather information for your area through Yahoo’s weather service, you will need to obtain a location ID or the URL to the RSS feed associated with your city. Before I explain the code line by line, I will list the steps to obtain the URL for the RSS feed of your local weather forecasts.

  1. Open browser to http://weather.yahoo.com/.
  2. Enter city or ZIP code and press Go button.
  3. Click the small orange colored RSS button near the right side of the web page (under “Add weather to your website”).
  4. Copy and paste the URL address line in your browser to be used in the code for your weather application. For example, I used the following RSS URL web address: http://weather.yahooapis.com/forecastrss?p=USMD0033&u=f.

Now that you have obtained a valid RSS URL web address, let’s use it in our recipe example. When creating the ManipulatingHtmlContent class, you will need two instance variables: url and refreshCountdown. The url variable will be assigned to the RSS URL web address from Step 4. The refreshCountdown variable of type int is assigned 60 to denote the time in seconds until a refresh or another retrieval of the weather information takes place.

Like all our JavaFX examples inside of the start() method, we begin by creating the Scene object for the initial main content region. Next, we create a javafx.scene.web.WebEngine instance by passing in the url into the constructor. The WebEngine object will asynchronously load the web content from Yahoo’s weather service. Later we will discuss the callback method responsible for handling the content when the web content is done loading. The following code line will create and load a URL web address using a WebEngine object:

   final WebEngine webEngine = new WebEngine(url);

After you create a WebEngine object, you will be creating an HTML document that will form as a template for later assembling when the web content is successfully loaded. Although the code contains HTML markup tags in Java code, which totally violates the principles of the separation of concerns, I inlined HTML by concatenating string values for brevity. To have a proper MVC-style separation, you may want to create a separate file containing your HTML content with substitution sections for data that will change over time. The code snippet that follows is the start of the creation of a template used to display weather information:

    StringBuilder template = new StringBuilder();
    template.append("<head> ")
       .append("<style type="text/css">body {background-color:#b4c8ee;}</style> ")
       .append("</head> ")
       .append("<body id='weather_background'>");

Once you have created your web page by concatenating strings, you will create a WebView object instance, which is a displayable graph node that will be responsible for rendering the web page. Remember from recipe 20-2, in which we discussed that a WebView will have its own instance of a WebEngine. Knowing this fact, we only use the WebView node to render the assembled HTML web page, not to retrieve the XML weather information via a URL. In other words, the WebEngine object is responsible for retrieving the XML from Yahoo’s Weather service to be parsed and then fed into the WebView object to be displayed as HTML. The following code snippet instantiates a WebView graph node that is responsible for rendering HTML5 content:

   final WebView webView = new WebView();

Next, you will create a countdown timer to refresh the weather information being displayed in the application window. First, you will instantiate an IntegerProperty variable, countdown, to hold the number of seconds until the next refresh time. Second, you will add a change listener (ChangeListener) to update the HTML content dynamically using JavaFX’s capability to execute JavaScript. The change listener also will determine whether the countdown has reached zero. If so, it will invoke the webEngine’s (WebEngine) reload() method to refresh or retrieve the weather information again. The following is the code that creates an IntegerProperty value to update the countdown text within the HTML using the executeScript() method:

   IntegerProperty countDown = new SimpleIntegerProperty(refreshCountdown);
   countDown.addListener(new ChangeListener<Number>() {
        @Override
        public void changed(ObservableValue<? extends Number> observable, Number oldValue,
   Number newValue){
   webView.getEngine().executeScript("document.getElementById('countdown').innerHTML =
   'Seconds till refresh: " + newValue + "'");
                   if (newValue.intValue() == 0) {
                       webEngine.reload();
                   }
               }
   }); // addListener()

After implementing your ChangeListener, you can create a TimeLine object to cause change on the countdown variable, thus triggering the ChangeListener to update the HTML text depicting the seconds until refresh. The follow code implements a TimeLine to update the countDown variable:

final Timeline timeToRefresh = new Timeline();
timeToRefresh.getKeyFrames().addAll(
    new KeyFrame(Duration.ZERO, new KeyValue(countDown, refreshCountdown)),
    new KeyFrame(Duration.seconds(refreshCountdown), new KeyValue(countDown, 0))
);

In summary, the rest of the code creates a ChangeListener that responds to a State.SUCCEEDED. Once the webEngine (WebEngine) has finished retrieving the XML, the change listener (ChangeListener) is responsible for parsing and rendering the assembled web page into the webView node. The following code parses and displays the weather data by calling the loadContent() method on the WebView’s WebEngine instance:

                if (newValue != State.SUCCEEDED) {
                    return;
                }
                Weather weather = parse(webEngine.getDocument());

                ...// the rest of the inlined HTML

                String countdownText = "<b id="countdown"></b><br /> ";
                webView.getEngine().loadContent(fullHtml + location.toString() +
                        timeOfWeatherTextDiv +
                        countdownText +
                        weather.htmlDescription);

To parse the XML returned by the webEngine’s getDocument() method, you will interrogate the org.w3c.dom.Document object. For convenience, I created a parse() method to walk the DOM to obtain weather data and return as a Weather object. See Javadocs and Yahoo’s RSS XML Schema for more information on data elements returned from weather service.

20-4. Responding to HTML Events

Problem

You begin to feel sorry for your other cube mates who are also oblivious to the outside world. A storm is approaching and you want to let them know to take their umbrella before leaving the building.

Solution

Add a Panic Button to your weather application that will simulate an e-mail notification. A Calm Down button is also added to retract the warning message.

The following code implements the weather application with additional buttons to warn and disregard a warning of impending stormy weather:

    @Override public void start(Stage stage) {

...  // template building

This code will add HTML buttons with the onclick attributes set to invoke the JavaScript alert function:

        template.append("<body id='weather_background'>");
        template.append("<form> ");
        template.append("  <input type="button" onclick="alert('warning')" value="Panic Button" /> ");
        template.append("  <input type="button" onclick="alert('unwarning')" value="Calm down" /> ");
        template.append("</form> ");

The following code is added to the start() method to create the warning message with opacity set as zero to be invisible:

        // calls the createMessage() method to build warning message
        final Text warningMessage = createMessage(Color.RED, "warning: ");
        warningMessage.setOpacity(0);

       ... // Countdown code

Continuing inside of the start() method, this code section is added to update the warning message after weather information was retrieved successfully:

        webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
            public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue){
                System.out.println("done!" + newValue.toString());
                if (newValue != State.SUCCEEDED) {
                    return;
                }
                Weather weather = parse(webEngine.getDocument());
                warningMessage.setText("Warning: " + weather.currentWeatherText + " Temp: " + weather.temperature + " E-mailed others");

                ... // the rest of changed() method
        }); // end of addListener method

This code sets the OnAlert property, which is an event handler to respond when a the Panic or Calm Down button is pressed:

        webView.getEngine().setOnAlert(new EventHandler<WebEvent<String>>(){
            public void handle(WebEvent<String> evt) {
                warningMessage.setOpacity("warning".equalsIgnoreCase(evt.getData()) ? 1d : 0d);
            }
        }); // end of setOnAlert() method.

        root.getChildren().addAll(webView, warningMessage);

        stage.setScene(scene);
        stage.show();

    } // end of start() method

The following method is code that you will add as a private method that is responsible for creating a text node (javafx.scene.text.Text) to be used as the warning message when the user presses the Panic Button:

    private Text createMessage(Color color, String message) {
        DropShadow dShadow = DropShadowBuilder.create()
                            .offsetX(3.5f)
                            .offsetY(3.5f)
                            .build();
        Text textMessage = TextBuilder.create()
                    .text(message)
                    .x(100)
                    .y(50)
                    .strokeWidth(2)
                    .stroke(Color.WHITE)
                    .effect(dShadow)
                    .fill(color)
                    .font(Font.font(null, FontWeight.BOLD, 35))
                    .translateY(50)
                    .build();
        return textMessage;
    }
} // end of the RespondingToHtmlEvents class

Figure 20-10 shows our weather application displaying a warning message after the Panic Button has been pressed. To remove the warning message, you can press the Calm Down button.

images

Figure 20-10. Weather application displaying warning message

How It Works

In this recipe you will add additional features to the weather application (from recipe 20-3) that responds to HTML events. The application you will be creating is similar to the previous recipe, except you will be adding HTML buttons on the web page to be rendered onto the WebView node. The first button added is the Panic Button that, when pressed, displays a warning message stating the current weather condition and a simulated e-mail notification to your cube mates. To retract the warning message you will also add a Calm Down button.

images Note Because the code is so similar to the previous recipe, I will point out the additions to the source code without going into great detail.

To add the buttons, you will use the HTML tag <input type=”button”…> with an onclick attribute set to use JavaScript’s alert() function to notify JavaFX of an alert event. Shown here are the two buttons added to the web page:

        StringBuilder template = new StringBuilder();
        ...// Header part of HTML Web page
        template.append("<form> ");
        template.append("  <input type="button" onclick="alert('warning')" value="Panic Button" /> ");
        template.append("  <input type="button" onclick="alert('unwarning')" value="Calm down" /> ");
        template.append("</form> ");

When the web page renders allowing you to press the buttons, the onclick attribute will call JavaScript’s alert() function that contains a string message. When the alert() function is invoked, the web page’s owning parent (the webView’sWebEngine instance) will be notified of the alert via the WebEngine’s OnAlert attribute. To respond to JavaScript’s alerts, you will add an event handler (EventHandler) to respond to WebEvent objects. In the handle() method, you will simply show and hide the warning message by toggling the opacity of the warningMessage node (javafx.scene.text.Text). The following code snippet toggles the opacity of the warning message based on comparing the event’s data (evt.getData()) that contains the string passed in from the JavaScript’s alert() function. So, if the message is “warning,” the warningMessage opacity is set to 1; otherwise, set to 0 (both of type double).

webView.getEngine().setOnAlert(new EventHandler<WebEvent<String>>(){
    public void handle(WebEvent<String> evt) {
       warningMessage.setOpacity("warning".equalsIgnoreCase(evt.getData()) ? 1d : 0d);
    }
});

Please see the Javadocs for additional HTML web events (WebEvent).

20-5. Displaying Content from the Database

Problem

You want to keep up on the latest news monitoring the local legislature and science regarding the detrimental effects of the lack of light in small cubical work areas.

Solution

Create a JavaFX RSS reader. The RSS feed location URLs will be stored in a database to be later retrieved. Listed here are the main classes used in this recipe:

  • javafx.scene.control.Hyperlink
  • javafx.scene.web.WebEngine
  • javafx.scene.web.WebView
  • org.w3c.dom.Document
  • org.w3c.dom.Node
  • org.w3c.dom.NodeList

This recipe will be using an embedded database called Derby from the Apache group at http://www.apache.org. As a requirement, you will need to download the Derby software. To download the software, visit http://db.apache.org/derby/derby_downloads.html to download the latest version containing the libraries. Once downloaded, you can unzip or untar into a directory. To compile and run this recipe, you will need to update the classpath in your IDE or environment variable to point to Derby libraries (derby.jar and derbytools.jar). When running the example code you can type into the text field a valid RSS URL and then hit the enter key to load your new RSS headlines. After loading is complete the headline news is listed to the upper right frame region. Next, you will have an opportunity to choose a headline news article to read fully by clicking on a view button beneath it.

The following code implements an RSS reader in JavaFX:

package org.java7recipes.chapter20.recipe20_05;

import java.util.*;
import javafx.application.Application;
import javafx.beans.value.*;
import javafx.collections.ObservableList;
import javafx.concurrent.Worker.State;
import javafx.event.*;
import javafx.geometry.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.web.*;
import javafx.stage.Stage;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;


/**
 * Display Contents From Database
 * @author cdea
 */
public class DisplayContentsFromDatabase extends Application {

    @Override public void start(Stage stage) {
        Group root = new Group();
        Scene scene = new Scene(root, 640, 480, Color.WHITE);
        final Map<String, Hyperlink> hyperLinksMap = new TreeMap<>();


        final WebView newsBrief = new WebView(); // upper right
        final WebEngine webEngine = new WebEngine();
        final WebView websiteView = new WebView(); // lower right

        webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
            public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue){
                if (newValue != State.SUCCEEDED) {
                    return;
                }

                RssFeed rssFeed = parse(webEngine.getDocument(), webEngine.getLocation());

                hyperLinksMap.get(webEngine.getLocation()).setText(rssFeed.channelTitle);

                // print feed info:
                StringBuilder rssSource = new StringBuilder();
                rssSource.append("<head> ")
                        .append("</head> ")
                        .append("<body> ");
                rssSource.append("<b>")
                         .append(rssFeed.channelTitle)
                         .append(" (")
                         .append(rssFeed.news.size())
                         .append(")")
                         .append("</b><br /> ");
                 StringBuilder htmlArticleSb = new StringBuilder();
                for (NewsArticle article:rssFeed.news) {

                    htmlArticleSb.append("<hr /> ")
                         .append("<b> ")
                         .append(article.title)
                         .append("</b><br />")
                         .append(article.pubDate)
                         .append("<br />")
                         .append(article.description)
                         .append("<br /> ")
                         .append("<input type="button" onclick="alert('")
                            .append(article.link)
                            .append("')" value="View" /> ");
                }

                String content = rssSource.toString() + "<form> " + htmlArticleSb.toString() + "</form></body> ";
                System.out.println(content);
                newsBrief.getEngine().loadContent(content);
                // write to disk if not already.
                DBUtils.saveRssFeed(rssFeed);
            }
        }); // end of webEngine addListener()

        newsBrief.getEngine().setOnAlert(new EventHandler<WebEvent<String>>(){
            public void handle(WebEvent<String> evt) {
                websiteView.getEngine().load(evt.getData());
            }
        }); // end of newsBrief setOnAlert()
        // Left and right split pane
        SplitPane splitPane = new SplitPane();
        splitPane.prefWidthProperty().bind(scene.widthProperty());
        splitPane.prefHeightProperty().bind(scene.heightProperty());

        final VBox leftArea = new VBox(10);
        final TextField urlField = new TextField();
        urlField.setOnAction(new EventHandler<ActionEvent>(){
            public void handle(ActionEvent ae){
                String url = urlField.getText();
                final Hyperlink jfxHyperLink = createHyperLink(url, webEngine);
                hyperLinksMap.put(url, jfxHyperLink);
                HBox rowBox = new HBox(20);
                rowBox.getChildren().add(jfxHyperLink);
                leftArea.getChildren().add(rowBox);
                webEngine.load(url);
                urlField.setText("");
            }
        }); // end of urlField setOnAction()

        leftArea.getChildren().add(urlField);

        List<RssFeed> rssFeeds = DBUtils.loadFeeds();
        for (RssFeed feed:rssFeeds) {
            HBox rowBox = new HBox(20);
            final Hyperlink jfxHyperLink = new Hyperlink(feed.channelTitle);
            jfxHyperLink.setUserData(feed);
            final String location = feed.link;
            hyperLinksMap.put(feed.link, jfxHyperLink);
            jfxHyperLink.setOnAction(new EventHandler<ActionEvent>() {
                    public void handle(ActionEvent evt) {
                        webEngine.load(location);
                    }
                }
            );
            rowBox.getChildren().add(jfxHyperLink);
            leftArea.getChildren().add(rowBox);

        } // end of for loop


        // Dragging over surface
        scene.setOnDragOver(new EventHandler<DragEvent>() {
            @Override
            public void handle(DragEvent event) {
                Dragboard db = event.getDragboard();
                if (db.hasUrl()) {
                    event.acceptTransferModes(TransferMode.COPY);
                } else {
                    event.consume();
                }
            }
        }); // end of scene.setOnDragOver()

        // Dropping over surface
        scene.setOnDragDropped(new EventHandler<DragEvent>() {

            @Override
            public void handle(DragEvent event) {
                Dragboard db = event.getDragboard();
                boolean success = false;
                HBox rowBox = new HBox(20);
                if (db.hasUrl()) {
                    if (!hyperLinksMap.containsKey(db.getUrl())) {
                        final Hyperlink jfxHyperLink = createHyperLink(db.getUrl(), webEngine);
                        hyperLinksMap.put(db.getUrl(), jfxHyperLink);
                        rowBox.getChildren().add(jfxHyperLink);
                        leftArea.getChildren().add(rowBox);
                    }
                    webEngine.load(db.getUrl());
                }
                event.setDropCompleted(success);
                event.consume();
            }
        });  // end of scene.setOnDragDropped()

        leftArea.setAlignment(Pos.TOP_LEFT);

        // Upper and lower split pane
        SplitPane splitPane2 = new SplitPane();
        splitPane2.setOrientation(Orientation.VERTICAL);
        splitPane2.prefWidthProperty().bind(scene.widthProperty());
        splitPane2.prefHeightProperty().bind(scene.heightProperty());

        HBox centerArea = new HBox();

        centerArea.getChildren().add(newsBrief);

        HBox rightArea = new HBox();

        rightArea.getChildren().add(websiteView);

        splitPane2.getItems().add(centerArea);
        splitPane2.getItems().add(rightArea);

        // add left area
        splitPane.getItems().add(leftArea);

        // add right area
        splitPane.getItems().add(splitPane2);
        newsBrief.prefWidthProperty().bind(scene.widthProperty());
        websiteView.prefWidthProperty().bind(scene.widthProperty());
        // evenly position divider
        ObservableList<SplitPane.Divider> dividers = splitPane.getDividers();
        for (int i = 0; i < dividers.size(); i++) {
            dividers.get(i).setPosition((i + 1.0) / 3);
        }

        HBox hbox = new HBox();
        hbox.getChildren().add(splitPane);
        root.getChildren().add(hbox);

        stage.setScene(scene);
        stage.show();


    } // end of start()

    private static RssFeed parse(Document doc, String location) {

        RssFeed rssFeed = new RssFeed();
        rssFeed.link = location;

        rssFeed.channelTitle = doc.getElementsByTagName("title")
             .item(0)
             .getTextContent();

        NodeList items = doc.getElementsByTagName("item");
        for (int i=0; i<items.getLength(); i++){
            Map<String, String> childElements = new HashMap<>();
            NewsArticle article = new NewsArticle();
            for (int j=0; j<items.item(i).getChildNodes().getLength(); j++) {
                Node node = items.item(i).getChildNodes().item(j);
                childElements.put(node.getNodeName().toLowerCase(), node.getTextContent());
            }
            article.title = childElements.get("title");
            article.description = childElements.get("description");
            article.link = childElements.get("link");
            article.pubDate = childElements.get("pubdate");

            rssFeed.news.add(article);
        }

        return rssFeed;
    } // end of parse()

    private Hyperlink createHyperLink(String url, final WebEngine webEngine) {
        final Hyperlink jfxHyperLink = new Hyperlink("Loading News...");
        RssFeed aFeed = new RssFeed();
        aFeed.link = url;
        jfxHyperLink.setUserData(aFeed);
        jfxHyperLink.setOnAction(new EventHandler<ActionEvent>() {
            public void handle(ActionEvent evt) {
                RssFeed rssFeed = (RssFeed)jfxHyperLink.getUserData();
                webEngine.load(rssFeed.link);
            }
        });
        return jfxHyperLink;
    } // end of createHyperLink()

    public static void main(String[] args){
        DBUtils.setupDb();
        Application.launch(args);
    }
}
class RssFeed {
    int id;
    String channelTitle = "News...";
    String link;
    List<NewsArticle> news = new ArrayList<>();

    public String toString() {
        return "RssFeed{" + "id=" + id + ", channelTitle=" + channelTitle + ", link=" + link + ", news=" + news + '}';
    }
    public RssFeed() {
    }
    public RssFeed(String title, String link) {
        this.channelTitle = title;
        this.link = link;
    }
}
class NewsArticle {
    String title;
    String description;
    String link;
    String pubDate;

    public String toString() {
        return "NewsArticle{" + "title=" + title + ", description=" + description + ", link=" + link + ", pubDate=" + pubDate + ", enclosure=" + '}';
    }

}

The following code is an exerpt from DBUtils.java showing the saveRssFeed() method which is responsible for persisting RSS feeds:

    public static int saveRssFeed(RssFeed rssFeed) {
        int pk = rssFeed.link.hashCode();

        loadDriver();

        Connection conn = null;
        ArrayList statements = new ArrayList();
        PreparedStatement psInsert = null;
        Statement s = null;
        ResultSet rs = null;
        try {

            // database name
            String dbName = "demoDB";

            conn = DriverManager.getConnection(protocol + dbName
                    + ";create=true", props);

            rs = conn.createStatement().executeQuery("select count(id) from rssFeed where id = " + rssFeed.link.hashCode());

            rs.next();
            int count = rs.getInt(1);

            if (count == 0) {

                // handle transaction
                conn.setAutoCommit(false);

                s = conn.createStatement();
                statements.add(s);

                psInsert = conn.prepareStatement("insert into rssFeed values (?, ?, ?)");
                statements.add(psInsert);
                psInsert.setInt(1, pk);
                String escapeTitle = rssFeed.channelTitle.replaceAll("'", "''");
                psInsert.setString(2, escapeTitle);
                psInsert.setString(3, rssFeed.link);
                psInsert.executeUpdate();
                conn.commit();
                System.out.println("Inserted " + rssFeed.channelTitle + " " + rssFeed.link);
                System.out.println("Committed the transaction");
            }
            shutdown();
        } catch (SQLException sqle) {
            sqle.printStackTrace();
        } finally {
            // release all open resources to avoid unnecessary memory usage

            // ResultSet
            close(rs);

            // Statements and PreparedStatements
            int i = 0;
            while (!statements.isEmpty()) {
                // PreparedStatement extend Statement
                Statement st = (Statement) statements.remove(i);
                close(st);
            }
            //Connection
            close(conn);

        }

        return pk;
    }

In Figure 20-11, our JavaFX reader displays three frames. The left column shows a text field at the top to allow the user to enter new urls and RSS feed sources as hyperlinks underneath. The upper-right frame contains the headline, an excerpt of the article, and a view button that renders the article’s web page in the bottom frame (lower-right region).

images

Figure 20-11. JavaFX RSS reader

Shown here is an example of output of the HTML to be rendered in the new headlines region (upper-right frame). You will also see the html view button responsible for notifying the application to load and render the entire article in the lower right frame region:

<head>
</head>
<body>
<b>Carl's FX Blog (10)</b><br />
<form>
<hr />
<b>
JavaFX Forms Framework Part 2</b><br />Mon, 03 Aug 2009 18:36:02 +0000<br />Introduction
This is the second installment of a series of blog entries relating to a proof of concept
for a JavaFX Forms Framework. Before I specify the requirements and a simple design of the
FXForms Framework, I want to follow-up on comments about tough issues relating to enterprise
application development and JavaFX. If you recall [...]<img alt="" border="0"
src="http://stats.wordpress.com/b.gif?host=carlfx.wordpress.com&amp;blog=6443320&amp;post=33
9&amp;subd=carlfx&amp;ref=&amp;feed=1" width="1" height="1" /><br />
<input type="button" onclick="alert('http://carlfx.wordpress.com/2009/08/03/javafx-forms-
framework-part-2/')" value="View" />

... // the rest of the headlines

</form></body>

How It Works

To create an RSS reader, you will need to store feed locations for later reading. When adding a new RSS feed, you will want to locate the little orange iconic button and drag the URL address line into your JavaFX RSS reader application. I find that the drag metaphor works on my FireFox browser. However, if dragging doesn’t work I’ve provided a text field to allow you to cut-and-paste the URL. Once the URL is entered you will hit the enter key to initiate the loading of the headline news. For example you can visit Google’s technology news RSS at:

http://news.google.com/news?pz=1&cf=all&ned=us&hl=en&topic=tc&output=rss.

Figure 20-12 depicts the orange RSS icon in the upper left.

images

Figure 20-12. RSS icon

Once the URL is accepted via drag-n-drop or text field, the JavaFX RSS reader application will save the URL location to a database. The RSS application consists of three frame regions: the RSS feed title column (left), headline news (upper right), and web site view (lower right). To display the news headlines, click the hyperlinks to the left. To show the entire article in the lower-right frame, click the View button below the headline in the upper-right frame. Before running the code, the application will require the jar libraries derby.jar and derbytools.jar included into your project classpath. These libraries allow you to save RSS URLs to an embedded JDBC database.

Similar to what you did in recipe 20-3, you retrieve news information from the Internet. The RSS retrieved will be using version 2.0. RSS is an XML standard providing really simple syndication, thus the acronym RSS. Now enough with the acronyms; let’s jump into the code, shall we?

In our start() method, you will create a 640 by 480 white scene display area. Next, you will create a map (TreeMap) containing Hyperlink objects as values and keys representing the URL location (String) to the RSS feed. As before when displaying HTML content, you will need to create WebViews. Here you will create two WebViews and one WebEngine. The two WebViews will render HTML for the news headline frame region and the viewing of the entire article region (lower right). The single WebEngine is responsible for retrieving the RSS feed when the user clicks the left frame region containing the RSS hyperlinks.

To support the feature that allows the user to enter an RSS feed you will need to create a text field that is able to save and render the headline news. Below is the code snippet to save an RSS URL and to add an address as a new hyperlink to the list of feeds.

        final VBox leftArea = new VBox(10);
        final TextField urlField = new TextField();
        urlField.setOnAction(new EventHandler<ActionEvent>(){
            public void handle(ActionEvent ae){
                String url = urlField.getText();
                final Hyperlink jfxHyperLink = createHyperLink(url, webEngine);
                hyperLinksMap.put(url, jfxHyperLink);
                HBox rowBox = new HBox(20);
                rowBox.getChildren().add(jfxHyperLink);
                leftArea.getChildren().add(rowBox);
                webEngine.load(url);
                urlField.setText("");
            }
        }); // end of urlField setOnAction()

After a user has clicked on a hyperlink the news retrieval is initiated. Once a successful retrieve has occurred on the webEngine (WebEngine) object, you will need to add a ChangeListener instance to respond when the state property changes to State.SUCCEEDED. With a valid state of State.SUCCEEDED, you will begin to parse the XML DOM returned from the WebEngine’s getDocument() method. Again, I provided a convenience method called parse() to interrogate the Document object representing the RSS news information.

   RssFeed rssFeed = parse(webEngine.getDocument(), webEngine.getLocation());

Next, you will create an HTML page that will list the channel tile and the number of total news headlines returned. After creating the HTML to display the RSS channel title and number of articles, you will iterate over all the news headlines to build record sets or rows. Each row will contain an HTML button labeled View to notify the WebEngine object of an alert containing the URL of the article. When the WebEngine object is notified, the OnAlert property will contain an event handler to render the entire article in the frame in the lower-right split region. After the web page is assembled, you will call the newsBrief object’s getEngine().loadContent() method to render the page. Once rendered you will save the URL rss Feed (RssFeed) object to the database by invoking the DBUtils.saveRssFeed(rssFeed). As a convenience, the saveRssFeed() method will check for duplicates and not save them. The following code loads the web page to be rendered and saves the newly added rss Feed URL:

   newsBrief.getEngine().loadContent(content);
   // write to disk if not already.
   DBUtils.saveRssFeed(rssFeed);

As in the previous recipes, you will be responding to HTML WebEvents when the new headline View button is pressed, which calls a JavaScript’s alert() function. Shown following is the code snippet to handle a web event (WebEvent) containing a string of the URL that links to the entire article to be viewed in the frame to the lower right region:

newsBrief.getEngine().setOnAlert(new EventHandler<WebEvent<String>>(){
            public void handle(WebEvent<String> evt) {
                websiteView.getEngine().load(evt.getData());
            }
        });

When creating the headlines region (upper right) containing HTML buttons to render the article’s web page, you will notice the alert() function containing the URL to be loaded and rendered in the lower bottom split frame region. Shown following is an example of HTML generated for an headline news containing a View button that can notify the web engine’s OnAlert web event (WebEvent).

   <input type="button" onclick="alert('http://carlfx.wordpress.com/2009/08/03/javafx-forms-
framework-part-2/')" value="View" />

One last thing to point out is that the RSS application has missing features. One feature that comes to my mind is the ability to delete individual RSS hyperlinks on the left column region. A workaround is to remove all links by deleting the database on the file system. Because Derby is an embedded database, you can delete the directory containing the database. The JavaFX RSS application will re-create an empty database if one doesn’t exist. Hopefully, you can add new features to enhance this fun and useful application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.244.94