Granite Data Services » JavaFX The Event-Driven, Cross-Platform Application Client Container Thu, 09 Jan 2014 15:55:39 +0000 en-US hourly 1 http://wordpress.org/?v=3.7.1 Migrating from GraniteDS 3.0.0.M3 to 3.0.0.RC1 (JavaFX) /2013/10/15/migrating-from-graniteds-3-0-0-m3-to-3-0-0-rc1-javafx/ /2013/10/15/migrating-from-graniteds-3-0-0-m3-to-3-0-0-rc1-javafx/#comments Tue, 15 Oct 2013 09:50:18 +0000 /?p=2165 Read More

]]>
There have been once again some changes in the client APIs which will require changing your code.

Package names

Several packages have been renamed to a more consistent convention under the prefix ‘org.granite.client.javafx’. Usually a simple ‘Optimize imports’ or search/replace in your IDE should be enough to do the necessary changes.

ChannelFactory API

The ChannelFactory API has been slighly changed to allow different channel types:

ChannelFactory channelFactory = new JMFChannelFactory();
channelFactory.start();
MessagingChannel channel = channelFactory.newMessagingChannel(ChannelType.LONG_POLLING,
    "longPollingChannel", "http://localhost:8080/gravityamf/amf.txt")
Consumer consumer = new Consumer(channel, "stocks", "europe");

For a websocket channel, you will also have to configure the transport:

ChannelFactory channelFactory = new JMFChannelFactory();
channelFactory.setMessagingTransport(ChannelType.WEBSOCKET, new JettyWebSocketTransport());
channelFactory.start();
MessagingChannel channel = channelFactory.newMessagingChannel(ChannelType.WEBSOCKET, 
    "websocketChannel", "ws://localhost:8080/myapp/gravityamf/amf");
Consumer consumer = new Consumer(channel, "stocks", "europe");

For a UDP channel, assuming the granite-client-java-udp.jar library is present:

ChannelFactory channelFactory = new JMFChannelFactory();
channelFactory.start();
MessagingChannel channel = channelFactory.newMessagingChannel(ChannelType.UDP, 
    "udpChannel", "http://localhost:8080/myapp/gravityamf/amf.txt")
Consumer consumer = new Consumer(channel, "stocks", "europe");

Of course you can mix all these channels and get them from the same ChannelFactory.

Additionally you can use the ServerApp API if you don’t want to build the url yourself:

ServerApp serverApp = new ServerApp("/myapp", false, "localhost", 8080);
ChannelFactory channelFactory = new JMFChannelFactory();
channelFactory.start();
MessagingChannel lpChannel = channelFactory.newMessagingChannel(ChannelType.LONG_POLLING, 
    "longPollingChannel", serverApp);
MessagingChannel wsChannel = channelFactory.newMessagingChannel(ChannelType.WEBSOCKET, 
    "websocketChannel", serverApp);
MessagingChannel udpChannel = channelFactory.newMessagingChannel(ChannelType.UDP, 
    "udpChannel", serverApp);

ServerSession API

The ServerSession API has also been updated.

You can now use the ServerSession to build Consumer and Producer objects:

ServerSession serverSession = new ServerSession("/myapp", "localhost", 8080);
serverSession.setMessagingTransport(ChannelType.WEBSOCKET, new JettyWebSocketTransport());
serverSession.start();
Consumer lpConsumer = serverSession.getConsumer(ChannelType.LONG_POLLING, "stocks", "europe");
Consumer wsConsumer = serverSession.getConsumer(ChannelType.WEBSOCKET, "stocks", "usa");
Consumer udpConsumer = serverSession.getConsumer(ChannelType.UDP, "stocks", "asia");

The GraniteDS Team.

]]>
/2013/10/15/migrating-from-graniteds-3-0-0-m3-to-3-0-0-rc1-javafx/feed/ 0
GraniteDS 3.0.0.RC1 is out /2013/10/15/graniteds-3-0-0-rc1-is-out/ /2013/10/15/graniteds-3-0-0-rc1-is-out/#comments Tue, 15 Oct 2013 09:50:00 +0000 /?p=2114 Read More

]]>
Granite Data Services 3.0.0.M3 is out and available for download . Maven artifacts are also available from the Maven central repository .

What's New?

The most visible change in this release is the new build process which is now entirely based on Gradle: instead of several Git repositories (one for each module), you will now find everything under a single repository called . The documentation is now based on , a much lighter solution than our previous docbook generation.

This heavy lifting of our build tools was long needed and should help us to bring more frequent releases. It will also make much easier to contribute to the project, either code or docs by simple pull requests in github.

On the technical features side, here are some highlights:

  • UDP support for real-time messaging (Flex, Java, JavaFX, Android).
  • Incremental serialization (aka ChangeSets), which only transmits a set of modified properties instead of complete object graphs (Flex only for now).
  • The Tide ServerSession API, previously only available on Java/JavaFX clients has been ported to Flex and allows Flex/Tide clients to connect to multiple servers.
  • The JMF protocol can now be used with WebSockets for very compact and fast serialization of messages (Java/JavaFX only).
  • You can serialize AS3 Vectors from the server with the help of specific annotations (Flex only).

Of course, many other bug fixes and improvements are also packaged in this release and you can find the complete list on Jira.

What's Next?

3.0.0.GA

We plan to release the final 3.0.0 version at early/mid November. The APIs are now final and we won’t add any new feature, but focus only on bug fixes, documentation, examples and tutorials. You are thus invited to use the RC1 as soon as possible and report quickly anything that looks like a bug.

3.1.0

The next major release will introduce at least these new features or improvements:

  • WebSocket 1.0 (JSR 356) client and server support for Java/JavaFX.
  • Incremental serialization (ChangeSets) for Java/JavaFX clients.
  • Improved Android support with data binding.
  • JSON support for HTML/JavaScript clients.

Licensing

You can refer to the previous post for the release announcement of 3.0.0.M3 for the new licensing of the different modules. To summarize, all server and basic libraries are now released under the LGPL 2.1 (previously LGPL 2.0), and all advanced client libraries and UDP client and server integration are released under a dual GPL 3.0 / commercial license.

All current users of GraniteDS can send a request at [email protected] for a free one year ‘early bird license’ for all these libraries until the final 3.0.0.GA release.

Migrating from GraniteDS 3.0.0.M3 to 3.0.0.RC1

There have been once again some changes in the client APIs which will require some changes in your code.

Be sure to check our posts about “Migrating from GraniteDS 3.0.0.M3 to 3.0.0.RC1 (Flex)” and “Migrating from GraniteDS 3.0.0.M3 to 3.0.0.RC1 (JavaFX)”.

Comments are welcome,

The GraniteDS Team.

]]>
/2013/10/15/graniteds-3-0-0-rc1-is-out/feed/ 4
JavaFX Tutorial Updated /2013/08/23/javafx-tutorial-updated/ /2013/08/23/javafx-tutorial-updated/#comments Fri, 23 Aug 2013 12:30:21 +0000 /?p=2097 Read More

]]>
Following the 3.0.0.M3 release, you can find an updated tutorial about GraniteDS / JavaFX data management on Github : just follow the instructions displayed on the landing page of the project (README.adoc).

]]>
/2013/08/23/javafx-tutorial-updated/feed/ 0
Introducing JMF (Java Messaging Format) /2013/08/22/introducing-jmf-java-messaging-format/ /2013/08/22/introducing-jmf-java-messaging-format/#comments Thu, 22 Aug 2013 12:01:56 +0000 /?p=1905 Read More

]]>
GraniteDS release 3.0.0.M2 introduced a new serialization format for Java clients which is largely inspired by (Action Message Format version 3). It is called JMF, which stands for Java Messaging Format, and it is now used, starting with GraniteDS 3.0.0.M3, as the standard serialization format between Java / JavaFX / Android client applications and GraniteDS servers.

Why JMF?

Basically because we love AMF but AMF isn’t good for Java client applications: with AMF, for example, all Java numeric types (byte, short, int, long, float, double, etc.) are serialized either as ActionScript int or Number, which leads to useless conversions in the context of a Java to Java data exchange and can even lead to a loss of precision (large long values cannot be represented as Numbers without truncation). Furthermore, all collections (java.util.*) are normalized to a single ArrayCollection type, BigDecimals are converted to Numbers, enum values to nothing standard (ActionScript has no enum types), etc.

Using AMF with Java clients is certainly possible, and we do have AMF support for Java clients in GraniteDS, but it is pointlessly complicated and, as a result, requires a full bunch of ugly workarounds.

That said, JMF is largely inspired by AMF and designed with the following goals in mind:

  • Compactness: serialized data must be as small as possible (even more than with AMF).
  • Completeness: circular references must be correctly handled, everything that should be serialized must be serialized.
  • Accuracy: no trans-typing, no pointless conversions, unless explicitly wanted.
  • Extensibility: it must be possible to plug dedicated codecs for specific data types.
  • Observability: data flow must be understandable by programs that have no knowledge of what is serialized.
  • Security: only data that are meant to be serialized must be serialized.
  • Speed: serialization must be as fast as possible (but without breaking the previous goals).

How to Use JMF?

You basically need to do nothing to use JMF: HTTP headers of serialized data, unless you have configured your Java client application to do otherwise, use a specific mime-type which tells the GraniteDS backend to switch to JMF instead of AMF serialization for the incoming request and corresponding response.

The JMF mime-type is currently “application/x-jmf+amf”, the “+amf” part stating that the messages inside the request are instances of flex.messaging.messages.Message (possibly enclosed in an AMF0 envelop). We are considering to use our own message envelops when using JMF and the mime-type is likely going to be “application/x-jmf” only in later versions.

The good thing with this mime-type switch mechanism is that you can have Java (or even JavaFX / Android) client applications using the exact same backend used by Flex client applications: there is nothing specific to configure on the server-side.

How does JMF Compare to AMF and Standard Java Serialization in Terms of Size?

With small messages such as a ping message (which purpose is to make sure that the server is alive and to negotiate serialization options), JMF vs. AMF vs. Standard Java Serialization (ie. java.io.ObjectOutputStream) gives these results:

  • JMF: 113 bytes.
  • AMF: 261 bytes.
  • Java Serialization: 894 bytes.

With bigger messages (a list of person with collections of contacts), you can typically get these results:

  • JMF: 2115 bytes.
  • AMF: 2749 bytes.
  • Java Serialization: 5278 bytes.

As a general result, but without conducting an extensive benchmark, we have found that:

  • The size of JMF encoded data can be up to 1/3 of the size of AMF encoded data.
  • The size of JMF encoded data can be up to 1/8 of the size of Java serialized data.
  • JMF encoding is always smaller than AMF or standard Java serialization encoding.

What about Security?

AMF serialization doesn’t state anything about what is (de)serializable or not: this lack of control can lead to a security breach that was discovered by Wouter Coekaerts and reported . Granite Data Services fixed this issue by adding a AMF3DeserializerSecurizer that controls which class can be instantiated at deserialization time (see this post about GraniteDS 2.2.1.GA).

Unlike AMF but just like the standard Java serialization, JMF only encodes primitive types and classes that implement java.io.Serializable. As a result, the above vulnerability doesn’t affect JMF, unless classes that shouldn’t be serialized implement (which is a design bug) java.io.Serializable.

Is there a public JMF specification?

Not yet. However, you are free to have look at the implementation, it’s fully open-source, just like the rest of the GraniteDS platform.

]]>
/2013/08/22/introducing-jmf-java-messaging-format/feed/ 1
GraniteDS 3.0.0.M3 is out /2013/08/20/graniteds-3-0-0-m3-is-out/ /2013/08/20/graniteds-3-0-0-m3-is-out/#comments Tue, 20 Aug 2013 15:44:48 +0000 /?p=2011 Read More

]]>
Granite Data Services 3.0.0.M3 is out and available for download . Maven artifacts are also available through Maven central repositories .

This new release is a very important milestone on our way to the final 3.0.0.GA version, both from technical and business point-of-views:

What's new on the technical side?

Beside the usual , the 3.0.0.M3 version of our platform brings the following new features (some of them were already present in previous milestones, though at an early development stage):

  • JavaFX support is maturing: you can benefit from all advanced features which were only available in Tide / Flex and a more efficient serialization protocol than AMF3 for Java clients (JMF – Java Messaging Format)
  • Android support is brand new in this release: you can already develop full featured mobile applications connected to a GraniteDS backend and this new feature also relies on the improved JMF protocol.
  • All Java client libraries are now built using : we are moving the build process of the all platform to Gradle and are planning to “gradlelize” everything before the final 3.0.0.GA.

Our focus for the next release (likely a 3.0.0.RC1) will be on Android, UDP-based real-time messaging and JSON support.

Documentation and tutorials need an update, we will post new resources as soon as they are available.

What's new on the business side?

GraniteDS has always been an avid supporter of open source software. Our business model so far has been to sale yearly subscriptions to an enterprise version of our platform, embedding some very advanced features absent from the community version, together with an access to professional support services.

While we have had and still have some success with this model, it is clearly targeted to mid / big organizations developing critical applications: most of our users are simply using the community version, either because they don’t need any support or can’t afford to pay for our enterprise subscription packages. As a result, generated revenues are below what we need to move to the next step and truly develop and promote GraniteDS in the long run.

After considering different options, we have decided to move to the following business model:

  • GraniteDS stays open source and is even more open source than ever: the few advanced features which were not available in the community platform are going to be in the same Github repository, just like everything else (planned for the 3.0.0.RC1).
  • Most of the platform is still released under the license, though we are going to move it to (version 2.0 is now largely outdated and so to say historical); Advanced libraries (Granite Android and JavaFX Libraries in 3.0.0.M3) are released under the license, with the option of purchasing a commercial license.
  • Support packages are still available, alongside the new commercial licenses: they are two different kind of things and the only purpose of the commercial license is to enable people to use our GPL 3.0 software without the legal requirement to distribute their products under a compatible open source license. However, commercial license fees will be waived for our current customers, provided they have a up-to-date yearly-subscription to one of our enterprise package and as long as they renew this subscription.
  • Commercial licenses are affordable and even free for early birds: see our pricing.

We sincerely hope that you will understand this move, which is, from our point-of-view, pretty fair: open source and free software is great and must stay free for people developing open source software as well. For those who develop commercial software, and unless they can live with the basic features of our platform, an affordable and predictable financial contribution is now asked.

Before asking us any question about this new business model / commercial license, please read our licensing FAQ and the license itself.

So, what's going to be LGPL and GPL?

In the 3.0.0.M3 release, only the Granite Android and JavaFX Libraries are released under GPL 3.0, with the option of purchasing a commercial license: this includes Tide for Java client applications, but the basic Java client features (eg. low-level remoting and real-time messaging) are still LGPL. Everything else in the platform is LGPL (server-side libraries, Flex client libraries, code generation tools).

Starting with the next 3.0.0.RC1 release, two other products will be available under the same dual-licensing scheme (GPL 3.0 / GraniteDS SLA):

  • Granite Advanced Flex Library: former enterprise-only features (such as differential serialization), ActionScript3 big numbers implementation, Flex validation (Bean Validation implementation on the Flex side) and Tide for Flex.
  • Granite UDP Libraries: UDP-based real-time messaging (for Flex and Java clients).

Again, we are and will be offering free commercial licenses for early birds, so it is not going to hurt your wallet if you fast enough (the final 3.0.0.GA won’t be released before early October at the earliest).

Comments are welcome,

The GraniteDS Team.

]]>
/2013/08/20/graniteds-3-0-0-m3-is-out/feed/ 3
Thoughts about JavaFX and Bean Validation /2012/11/29/thoughts-about-javafx-and-bean-validation/ /2012/11/29/thoughts-about-javafx-and-bean-validation/#comments Thu, 29 Nov 2012 14:46:25 +0000 /?p=1853 Read More

]]>
Coming from Adobe Flex, it has been quite a surprise to discover that JavaFX 2 does not provide anything particular concerning input validation.

However we can make use of the existing UI event model to execute validations on the text inputs.

In a first naive implementation, we could simply think of an event handler that will manually validate the input on the fly :

textField.setOnKeyTyped(new EventHandler<KeyEvent>() {

    @Override
    public void handle(KeyEvent event) {
        String text = ((TextInputControl)event.getTarget()).getText());
        if (text.length() < 2 || text.length() > 25)
            ((Node)event.getTarget()).setStyle("-fx-border-color: red");
        else
            ((Node)event.getTarget()).setStyle("-fx-border-color: null");
    }
});

Here we have processed the validation manually, but we now would like to be able to leverage the Bean Validation API to simplify this.

Bean Validation is not meant to be applied on individual values (as the value of the input) but on data beans holding the validation constraint annotations.

However in general a text field would be used to populate a data bean, so we could do something like this :

public class Person {

    @Size(min=2, max=20)
    private String name;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

Having defined this bean, we could change the previous code by this :

final Validator validator = validatorFactory.getValidator(Person.class);

textField.setOnKeyTyped(new EventHandler<KeyEvent>() {

    @Override
    public void handle(KeyEvent event) {
        String text = ((TextInputControl)event.getTarget()).getText());
        Set<ConstraintViolation<Person>> violations = 
            validator.validateValue(Person.class, "name", text);

        if (!violations.isEmpty())
            ((Node)event.getTarget()).setStyle("-fx-border-color: red");
        else
            ((Node)event.getTarget()).setStyle("-fx-border-color: null");
    }
});

This is definitely better, but we still have to do this for each and every input field, which is not very convenient.

One possibility would be to write a reusable validation handler, like this :

public class ValidationEventHandler<T> extends EventHandler<KeyEvent> {

    private Class<T> beanClass;
    private String propertyName;

    public ValidationEventHandler(Class<T> beanClass, String propertyName) {
        this.beanClass = beanClass;
        this.propertyName = propertyName;
    }

    @Override
    public void handle(KeyEvent event) {
        String text = ((TextInputControl)event.getTarget()).getText());
        Set<ConstraintViolation<T>> violations = 
            validator.validateValue(beanClass, propertyName, text);

        if (!violations.isEmpty())
            ((Node)event.getTarget()).setStyle("-fx-border-color: red");
        else
            ((Node)event.getTarget()).setStyle("-fx-border-color: null");
    }
}

Which would lead to this on each field :

textField.setOnKeyTyped(
    new ValidationEventHandler<Person>(Person.class, "name"));

Still we have to define this handler on each input field, so it can be configured with the bean class and property.

If we want to go a bit further, we will need a generic way to determine the links between input fields and beans.

This link is application-specific, but if the application uses data binding, we can maybe find this information.

textField.text().bindBidirectional(person.name());

Unfortunately, there does not seem to be any generic reflection API on property bindings. Using the following extremely ugly hack we can find the target of a particular binding:

private Property<?> lookupBindingTarget(Property<?> inputProperty) {
    try {
        Field fh = inputProperty.getClass().getDeclaredField("helper");
        fh.setAccessible(true);

        Object helper = fh.get(inputProperty);
        Field fcl = helper.getClass().getDeclaredField("changeListeners");
        fcl.setAccessible(true);

        Object changeListeners = fcl.get(helper);
        if (changeListeners != null && Array.getLength(changeListeners) > 0) {
            ChangeListener<?> cl = 
                (ChangeListener<?>)Array.get(changeListeners, 0);

            try {
                Field fpr = cl.getClass().getDeclaredField("propertyRef2");
                fpr.setAccessible(true);

                WeakReference<?> ref= (WeakReference<?>)fpr.get(cl);
                Property<?> p = (Property<?>)ref.get();
                return p;
            }
            catch (NoSuchFieldException e) {
                 log.debug("Field propertyRef2 not found on " + cl, e);
                 return null;
            }
        }

        log.debug("Could not find target for property %s", inputProperty);
	return null;
    }
    catch (Exception e) {
        log.warn(e, "Could not find target for property %s", inputProperty);
        return null;
    }
}

Using this hack, We are now able to determine automatically the beans to validate for all input fields in a form.

This is exactly what the FormValidator class provided by GraniteDS is built to do. It scans all inputs in a JavaFX container and add listeners on them. It then calls the corresponding validator on the target bean and propagates the constraint violations to the input by dispatching a particular event of type ValidationResultEvent.

Using FormValidator with data binding, you would simply have to do this:

FormValidator formValidator = new FormValidator();
formValidator.setForm(formContainer);

Then all bound input fields will be validated on-the-fly depending on the Bean Validation constraints defined on the target beans.

The FormValidator dispatches validation events on the container so you can react and display hints and error messages to the user:

formContainer.addEventHandler(ValidationResultEvent.ANY,
    new EventHandler<ValidationResultEvent>() {
        @Override
        public void handle(ValidationResultEvent event) {
            if (event.getEventType() == ValidationResultEvent.INVALID)
                ((Node)event.getTarget()).setStyle("-fx-border-color: red");
            else if (event.getEventType() == ValidationResultEvent.VALID)
                ((Node)event.getTarget()).setStyle("-fx-border-color: null");
    }
});

Note that for now, the FormValidator requires that the data bean implements the GraniteDS interface DataNotifier which basically means that the bean is able to dispatch JavaFX events. This requirement could be possibly removed in a future release.

public interface DataNotifier extends EventTarget {

    public <T extends Event> void addEventHandler(EventType<T> type, 
        EventHandler<? super T> handler);

    public <T extends Event> void removeEventHandler(EventType<T> type, 
        EventHandler<? super T> handler);
}

The bean would then be implemented like this :

public class Person {

    private EventHandlerManager __handlerManager 
        = new EventHandlerManager(this); 

    @Override
    public EventDispatchChain buildEventDispatchChain(EventDispatchChain t) {
        return t.prepend(__handlerManager);
    }

    public <T extends Event> void addEventHandler(EventType<T> type, 
        EventHandler<? super T> handler) {
        __handlerManager.addEventHandler(type, handler);
    }

    public <T extends Event> void removeEventHandler(EventType<T> type, 
        EventHandler<? super T> handler) {
        __handlerManager.removeEventHandler(type, handler);
    }
}

This is once again not ideal but this can be easily extracted in an abstract class.

The validation step of the FormValidator is as follows:

Set<ConstraintViolation<Object>> allViolations 
    = validatorFactory.getValidator().validate((Object)entity, groups);

Map<Object, Set<ConstraintViolation<?>>> violationsMap 
    = new HashMap<Object, Set<ConstraintViolation<?>>>();

for (ConstraintViolation<Object> violation : allViolations) {
    Object rootBean = violation.getRootBean();
    Object leafBean = violation.getLeafBean();

    Object bean = leafBean != null 
        && leafBean instanceof DataNotifier ? leafBean : rootBean;

    Set<ConstraintViolation<?>> violations = violationsMap.get(bean);
    if (violations == null) {
        violations = new HashSet<ConstraintViolation<?>>();
        violationsMap.put(bean, violations);
    }			

    violations.add(violation);
}

for (Object bean : violationsMap.keySet()) {
    if (bean instanceof DataNotifier) {
        ConstraintViolationEvent event = 
            new ConstraintViolationEvent(
                ConstraintViolationEvent.CONSTRAINT_VIOLATION, 
                violationsMap.get(bean)
            );
        Event.fireEvent((DataNotifier)bean, event);
    }
}

Obviously that would be easier if we could plug in the bean validation lifecycle of each bean and fire the events during the validation itself, itself of doing this kind of postprocessing.

Conclusion

We have already most of the building blocks to integrate JavaFX and Bean Validation but it is definitely not as smooth as it should be.

The main pain points are the following :

  1. No way to plug in the validation lifecycle of Bean Validation
  2. No easy-to-use JavaFX UI components to display error popups
  3. No reflection API on the JavaFX data binding
  4. No simple event dispatch facility for beans

In fact, the two last points could be totally unneccessary if the validation was directly built-in the data binding feature, much like converters.

It is probably possible to build something like a ValidatableStringProperty, we will let this for a future post.

]]>
/2012/11/29/thoughts-about-javafx-and-bean-validation/feed/ 2
Data Management Tutorial with JavaFX 2.2 /2012/11/26/data-management-tutorial-with-javafx-2-2/ /2012/11/26/data-management-tutorial-with-javafx-2-2/#comments Mon, 26 Nov 2012 18:10:54 +0000 /?p=1593 Read More

]]>
Following the release of GraniteDS for JavaFX, here is the version of the data management tutorial for this new view technology. What we call data management is a set of features that allow to work easily with data objects on the client and simplify the boilerplate due to client/server integration.

The example project is hosted on GitHub at and requires Maven 3.x for building. It is also required to use a JDK 1.7.0_07 or better so JavaFX is already installed on the Java runtime. You may have to change your JAVA_HOME environment variable if you have many JDK installed.

It may be useful to follow the tutorial that you have an Eclipse installation, ideally with the M2E plugin installed (to automatically update Maven dependencies). Spring Tool Suite is a good choice, all the more that we are going to use the Spring framework on both the server and the client.

For the impatient, you can simply clone the project and build it.

From a console, type the following:

git clone git://github.com/graniteds/shop-admin-javafx.git
cd shop-admin-javafx
mvn install

To start the embedded Jetty server, type this:

cd webapp
mvn jetty:run

To start the JavaFX client application, open a second console and type the following:

cd shop-admin-javafx/javafx
java -jar target/shop-admin-javafx.jar

When the application shows up, just logon as admin/admin. It’s a simple CRUD example which allows searching, creating and modifying vineyards and the wines they produce.

The application is definitely ugly but its goal is simply to demonstrate the following features :

  • Basic CRUD with a Spring Data JPA repository
  • Support for lazy-loading of JPA x-to-many associations
  • Dirty-checking / Undo
  • Client validation with Bean Validation API
  • Security
  • Real-time push

For the sake of simplicity, this tutorial has a few known limitations:

  • The search is case-sensitive
  • Validation errors are displayed as simple red borders without the corresponding message
  • The ‘Save’ button is incorrectly enabled when something is typed in the search input

Each step corresponds to a tag on the GitHub project so you can see what has been changed at each step.

So now let’s start from scratch :

Step 1 : Create the Project from the Maven Archetype

If you have cloned the project from GitHub, just do this:

git checkout step1

This first step has been simply created by issuing the following command :

mvn archetype:generate
-DarchetypeGroupId=org.graniteds.archetypes
-DarchetypeArtifactId=org.graniteds-tide-spring-jpa
-DarchetypeVersion=2.0.0.M1
-DgroupId=com.wineshop
-DartifactId=shop-admin-javafx
-Dversion=1.0-SNAPSHOT
-Dpackage=com.wineshop

If you look at the result, the archetype has created a Maven project with three modules :

  • A Spring server module
  • A Webapp module
  • A JavaFX client module

The Spring server module includes a suitable persistence.xml JPA descriptor and a basic domain model and service (Welcome + WelcomeService).

The Webapp module includes the necessary Spring configuration for JPA with a simple HQL datasource, Spring Security and GraniteDS. It also includes a web.xml configured with a Spring dispatcher servlet, and Gravity Comet and WebSocket configurations for Jetty 8. The pom.xml of this module also includes the necessary configuration to run an embedded Jetty 8.

Finally the JavaFX module includes a pom.xml with the configuration to generate JavaFX client model from the JPA model with the GraniteDS gfx ant task, and to package the application as an executable jar. It also includes a skeleton JavaFX client application with the necessary Spring container and GraniteDS configurations.

The generated application is basically a slighly improved Hello World where the names that are sent are stored in the database and pushed to all clients.

You can check the push by running many JavaFX clients simultaneously.

To start the embedded Jetty server, type this:

cd webapp
mvn jetty:run

To start a JavaFX client application, open another console and type the following:

cd shop-admin-javafx/javafx
java -jar target/shop-admin-javafx.jar

You can log in with admin/admin or user/user.

Before continuing, we will just remove some of this initially generated stuff from the project and eclipsify it.

The tag step1b contains a cleaned up version of the project that you can import in Eclipse (actually you will get 4 projects).

git checkout step1b

Step 2 : Implement Basic CRUD Functionality

This will be the longest step as we are creating most of the application.

The Server Application

First we have to build the server application. For convenience we are going to use the new support for Spring Data JPA repositories, and simply define a JPA model.

com/wineshop/entities/Vineyard.java:

@Entity
public class Vineyard extends AbstractEntity {

    private static final long serialVersionUID = 1L;

    @Basic
    private String name;

    @Embedded
    private Address address = new Address();

    @OneToMany(cascade=CascadeType.ALL, mappedBy="vineyard",
        orphanRemoval=true)
    private Set wines;

    public String getName() {
        return name;
    }

    public void setName(String nom) {
        this.name = nom;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public Set getWines() {
        return wines;
    }

    public void setWines(Set wines) {
        this.wines = wines;
    }
}

com/wineshop/entities/Wine.java:

@Entity
public class Wine extends AbstractEntity {

    private static final long serialVersionUID = 1L;

    public static enum Type {
        RED,
        WHITE,
        ROSE
    }

    @ManyToOne
    private Vineyard vineyard;

    @Basic
    private String name;

    @Basic
    private Integer year;

    @Enumerated(EnumType.STRING)
    private Type type;

    public Vineyard getVineyard() {
        return vineyard;
    }

    public void setVineyard(Vineyard vineyard) {
        this.vineyard = vineyard;
    }

    public Integer getYear() {
        return year;
    }

    public void setYear(Integer annee) {
        this.year = annee;
    }

    public String getName() {
        return name;
    }

    public void setName(String nom) {
        this.name = nom;
    }

    public Type getType() {
        return type;
    }

    public void setType(Type type) {
        this.type = type;
    }
}

com/wineshop/entities/Address.java:

@Embeddable
public class Address implements Serializable {

    private static final long serialVersionUID = 1L;

    @Basic
    private String address;

    public String getAddress() {
        return address;
    }

    public void setAddress(String adresse) {
        this.address = adresse;
    }
}

Note that our entities extend the AbstractEntity that is provided by the archetype.

AbstractEntity simply has a Long id, a Long version field and a uid field. We will mostly replace it by AbstractPersistable from Spring Data but we have to keep it because of the uid property. The uid field is a global persistent identifier that is to be unique among all client and server layers, and is thus persisted in the database, but is not necessarily a database key. The GraniteDS data management framework can work without a specific uid field but with some restrictions (see the documentation for further explanation).

Next we are going to define the Spring Data repository, but first we have to add Spring Data in our dependencies in java/pom.xml.

<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-jpa</artifactId>
  <version>1.2.0.RELEASE</version>
<dependency>

We change our AbstractEntity to extends AbstractPersistable (not necessary useful but more Spring Data-esque):

@MappedSuperclass
@EntityListeners({AbstractEntity.AbstractEntityListener.class,
    DataPublishListener.class})
public abstract class AbstractEntity extends AbstractPersistable {

    private static final long serialVersionUID = 1L;

    /* "UUID" and "UID" are Oracle reserved keywords -> "ENTITY_UID" */
    @Column(name="ENTITY_UID", unique=true, nullable=false,
        updatable=false, length=36)
    private String uid;

    @Version
    private Integer version;

    public Integer getVersion() {
        return version;
    }

    @Override
    public boolean equals(Object o) {
        return (o == this || (o instanceof AbstractEntity
            && uid().equals(((AbstractEntity)o).uid())));
    }

    @Override
    public int hashCode() {
        return uid().hashCode();
    }

    public static class AbstractEntityListener {

        @PrePersist
        public void onPrePersist(AbstractEntity abstractEntity) {
            abstractEntity.uid();
        }
    }

    private String uid() {
        if (uid == null)
            uid = UUID.randomUUID().toString();
        return uid;
    }
}

And define the repository interface :

com/wineshop/services/VineyardRepository.java

@RemoteDestination
@DataEnabled
public interface VineyardRepository
    extends FilterableJpaRepository {
}

As you can see, this repository extends the GraniteDS-specific FilterableJpaRepository which is an extension of the default Spring JpaRepository that adds an extra finder method, findByFilter. This findByFilter is a kind of find by example implementation. We might possibly consider contributing this code to Spring Data JPA later to avoid this dependency on a GraniteDS implementation (or ditch it completely if Spring Data comes with something similar in a future release).

The @RemoteDestination annotation indicates that the repository is remote-enabled, that is will be accessible from our GraniteDS client. In general this is not what you would do for obvious security reasons (and for example create a Service in front of the repository) but we want to keep it simple here.

The @DataEnabled annotation indicates that GraniteDS should track JPA data updates happening during the execution of the service methods and propagate them to the client.

Finally we register our repository in webapp/src/main/webapp/WEB-INF/spring/app-jpa-config.xml:

<jpa:repositories
    base-package="com.wineshop.services"
    factory-class="org.granite.tide.spring.data.FilterableJpaRepositoryFactoryBean"/>

There is a tag step2a on the git project so you can see what has been changed since step 1.

git checkout step2a

Here the compare view on GitHub:

Now you can rebuild and restart the Jetty server:

mvn install
cd webapp
mvn jetty:run

You may have noticed that the gfx generator is ran as part of the maven build. If you have a look at the JavaFX module, you can see some newly generated classes for the client entities and a client proxy for the Spring Data repository, in the packages com.wineshop.client.entities and com.wineshop.client.services. That will be useful for the next part, that is developing the client.

The JavaFX Client

Now the most interesting part, the JavaFX client. To simplify things, we are going to keep some elements of the skeleton application, the Main and the Login screen. We are mostly going to work on the Home.fxml screen and Home.java controller.

Our UI will be very basic, a table view to display the list of vineyards, and a form to create or modify them.

The Table View

First we add the table.

Here is the relevant part of Home.fxml:

<!-- Search Bar -->
<HBox spacing="10">
    <children>
        <TextField fx:id="fieldSearch" prefColumnCount="20"
            onAction="#search"/>
        <Button text="Search" onAction="#search"/>
    </children>
</HBox>
 
<TableView fx:id="tableVineyards" layoutX="10" layoutY="40"
    items="$vineyards">
    <columns>
        <TableColumn fx:id="columnName" text="Name"
            prefWidth="320" sortable="true">
            <cellValueFactory>
                <PropertyValueFactory property="name"/>
            </cellValueFactory>
        </TableColumn>
    </columns>
</TableView>

Nothing very fancy, we simply define a table view control with a single column mapped to the name property of our Vineyard entity, and a search bar with a seach field and a search button.

The data source for the table view is defined as $vineyards, that would usually mean that we have to bind it to a collection in the controller. If you look at the Main class coming from the archetype, it uses the custom TideFXMLLoader that automatically exposes all beans in the Spring context as FXML variables. So we create a Spring bean named vineyards in the application configuration in Main.java:

@Bean
public PagedQuery vineyards(ServerSession serverSession)
    throws Exception {

    PagedQuery vineyards =
        new PagedQuery(serverSession);

    vineyards.setMethodName("findByFilter");
    vineyards.setMaxResults(25);
    vineyards.setRemoteComponentClass(VineyardRepository.class);
    vineyards.setElementClass(Vineyard.class);
    vineyards.setFilterClass(Vineyard.class);

    return vineyards;
}

We use the GraniteDS PagedQuery component which wires a client observable list (itself) to a server finder method, here the findByFilter method of the Spring Data repository. This component also handles paging automatically so we can define a maxResults property which defines the maximum number of elements that will be retrieved from the server at each remote call.

PagedQuery also handles remote filtering and sorting, so we next have to wire it to the view in the Home.java controller initialization:

@Inject
private PagedQuery vineyards;

The PagedQuery is a Spring bean, the Home controller too, so we can inject one into the other as needed.

@Override
public void initialize(URL url, ResourceBundle rb) {
    vineyards.getFilter().nameProperty()
        .bindBidirectional(fieldSearch.textProperty());
    vineyards
        .setSort(new TableViewSort(tableVineyards, Vineyard.class));
    ...
}

These declarations bind the filter name property to the search field and define the sort adapter between the TableView control and the PagedQuery component.

Finally we define the search action on the controller, it just has to call the method refresh on the PagedQuery component that will trigger a remote call to get an up-to-date dataset:

@FXML
private void search(ActionEvent event) {
    vineyards.refresh();
}

With this quite simple setup, we have a fully functional table view on our remote Vineyard entity.

The Edit Form

Here is the form description in Home.fxml:

<Label fx:id="labelFormVineyard" text="Create vineyard"/>
<GridPane fx:id="formVineyard" hgap="4" vgap="4">
    <children>
        <Label text="Name"
            GridPane.columnIndex="1" GridPane.rowIndex="1"/>
        <TextField fx:id="fieldName"
            GridPane.columnIndex="2" GridPane.rowIndex="1"/>
 
        <Label text="Address"
            GridPane.columnIndex="1" GridPane.rowIndex="2"/>
        <TextField fx:id="fieldAddress"
            GridPane.columnIndex="2" GridPane.rowIndex="2"/>
    </children>
</GridPane>
<!-- Button Bar -->
<HBox spacing="10">
    <children>
        <Button fx:id="buttonSave" text="Save"
            onAction="#save"/>
        <Button fx:id="buttonDelete" text="Delete"
            onAction="#delete" visible="false"/>
        <Button fx:id="buttonCancel" text="Cancel"
            onAction="#cancel" visible="false"/>
    </children>
</HBox>

Once again, nothing very spectacular. We now have to wire it to the controller, so we first add an instance variable vineyard that will hold the currently edited entity.

@FXML
private Vineyard vineyard;

Not very useful by itself, we now have to manage this variable correctly. We will do this in a method that will bind the form to an existing instance or create a new instance:

private void select(Vineyard vineyard) {
    if (vineyard == this.vineyard && this.vineyard != null)
        return;

    if (this.vineyard != null) {
        fieldName.textProperty()
            .unbindBidirectional(this.vineyard.nameProperty());
        fieldAddress.textProperty()
            .unbindBidirectional(this.vineyard.getAddress().addressProperty());
    }

    if (vineyard != null)
        this.vineyard = vineyard;
    else {
        this.vineyard = new Vineyard();
        this.vineyard.setName("");
        this.vineyard.setAddress(new Address());
        this.vineyard.getAddress().setAddress("");
    }

    fieldName.textProperty()
        .bindBidirectional(this.vineyard.nameProperty());
    fieldAddress.textProperty()
        .bindBidirectional(this.vineyard.getAddress().addressProperty());

    labelFormVineyard.setText(vineyard != null
        ? "Edit vineyard" : "Create vineyard");

    buttonDelete.setVisible(vineyard != null);
    buttonCancel.setVisible(vineyard != null);
}

Additionally this method changes the title of the form to ‘edit’ or ‘create’ and makes the delete and cancel buttons visible when working on an existing instance.

We will just call this method at the initialization of the screen and define a selection listener on the table to bind the selection to the form:

public void initialize(URL url, ResourceBundle rb) {

    ...

    select(null);
    tableVineyards.getSelectionModel().selectedItemProperty()
        .addListener(new ChangeListener() {

        @Override
        public void changed(ObservableValue<? extends Vineyard> property,
            Vineyard oldSelection, Vineyard newSelection) {
            select(newSelection);
        }
    });
}

We’re almost done, finally we have to define the actions of the three buttons:

@FXML
private void save(ActionEvent event) {
    final boolean isNew = vineyard.getId() == null;
    vineyardRepository.save(vineyard,
        new SimpleTideResponder() {

            @Override
            public void result(TideResultEvent tre) {
                if (isNew)
                    select(null);
                else
                    tableVineyards.getSelectionModel()
                        .clearSelection();
            }

            @Override
            public void fault(TideFaultEvent tfe) {
                System.out.println("Error: "
                    + tfe.getFault().getFaultDescription());
            }
        }
    );
}

Basically we save the entity by calling the remote Spring Data repository that we got injected by Spring. Note that a suitable client has been generated for the repository and is defined as a Spring bean.

On successful return, we either create a new empty entity with select(null) or simply clear the table selection, which will consequently clear the form and reset it in creation mode.

The delete action is quite similar:

@FXML
private void delete(ActionEvent event) {
    vineyardRepository.delete(vineyard.getId(),
        new SimpleTideResponder() {

            @Override
            public void result(TideResultEvent tre) {
                tableVineyards.getSelectionModel().clearSelection();
            }
        }
    );
}

The cancel operation is very basic for now:

@FXML
private void cancel(ActionEvent event) {
    tableVineyards.getSelectionModel().clearSelection();
}

You can certainly notice that we call the remote repository and don’t care about the actual result of the operation. In fact we don’t have because GraniteDS listens to the JPA events and propagates them to the client as Spring application events. The PagedQuery automatically listens to these client events and refreshes itself when needed. Of course if you need to access the result objects, you can still do it in the result handler.

The first step of the client application is now ready. You can get it with the tag step2 in the git repository:

git checkout step2

Here the compare view on GitHub:

You can now build it and run it, assuming your Jetty server it still running in another console:

cd javafx
mvn clean install
java -jar target/shop-admin-javafx.jar

Step 3: Support for JPA lazy associations

If you are still here, you have maybe noticed that we simply didn’t take care of the wines association. It is never populated, saved or rendered and that caused no problem to the application. GraniteDS is indeed able to properly serialize and deserialize all lazy association so you simply don’t have to care about them. What is lazy on the server stays lazy on the client.

Now we would like to edit the list of wines for our vineyards. We first add a list view to the edit form:

<Label text="Wines" GridPane.columnIndex="1" GridPane.rowIndex="3" />
<HBox spacing="5" GridPane.columnIndex="2" GridPane.rowIndex="3">
    <children>
        <ListView fx:id="listWines" maxHeight="150"/>
 
        <VBox spacing="5">
            <children>
                <Button text="+" onAction="#addWine"/>
                <Button text="-" onAction="#removeWine"/>
            </children>
        </VBox>
    </children>
</HBox>

Now in the controller, we have to bind the list of wines of the current edited vineyard to this list:

@FXML
private ListView listWines;
private void select(Vineyard vineyard) {
    ...
    listWines.setItems(this.vineyard.getWines());
    ...
}

And add the actions to add and remove a wine from the list:

@FXML
private void addWine(ActionEvent event) {
    Wine wine = new Wine();
    wine.setVineyard(this.vineyard);
    wine.setName("");
    wine.setYear(Calendar.getInstance().get(Calendar.YEAR)-3);
    wine.setType(Wine$Type.RED);
    this.vineyard.getWines().add(wine);
}

@FXML
private void removeWine(ActionEvent event) {
    if (!listWines.getSelectionModel().isEmpty())
        this.vineyard.getWines().remove(listWines.getSelectionModel().getSelectedIndex());
}

Finally we have to setup the list to display and edit the properties of the Wine objects:

listWines.setCellFactory(new Callback, ListCell>() {
    public ListCell call(ListView listView) {
        return new WineListCell();
    }
});
private static class WineListCell extends ListCell {

    private ChoiceTypeListener choiceTypeListener = null;

    protected void updateItem(Wine wine, boolean empty) {
        Wine oldWine = getItem();

        if (oldWine != null && wine == null) {
            HBox hbox = (HBox)getGraphic();

            TextField fieldName = (TextField)hbox.getChildren().get(0);
            fieldName.textProperty()
                .unbindBidirectional(getItem().nameProperty());

            TextField fieldYear = (TextField)hbox.getChildren().get(1);
            fieldYear.textProperty()
                .unbindBidirectional(getItem().yearProperty());

            getItem().typeProperty().unbind();
            getItem().typeProperty().removeListener(choiceTypeListener);
            choiceTypeListener = null;

            setGraphic(null);
        }

        super.updateItem(wine, empty);

        if (wine != null && wine != oldWine) {
            TextField fieldName = new TextField();
            fieldName.textProperty()
                .bindBidirectional(wine.nameProperty());

            TextField fieldYear = new TextField();
            fieldYear.setPrefWidth(40);
            fieldYear.textProperty()
                .bindBidirectional(wine.yearProperty(), new IntegerStringConverter());

            ChoiceBox choiceType = new ChoiceBox(
                FXCollections.observableArrayList(Wine$Type.values())
            );
            choiceType.getSelectionModel()
                .select(getItem().getType());
            getItem().typeProperty()
                .bind(choiceType.getSelectionModel().selectedItemProperty());

            choiceTypeListener = new ChoiceTypeListener(choiceType);
            getItem().typeProperty()
                .addListener(choiceTypeListener);

            HBox hbox = new HBox();
            hbox.setSpacing(5.0);
            hbox.getChildren().add(fieldName);
            hbox.getChildren().add(fieldYear);
            hbox.getChildren().add(choiceType);
            setGraphic(hbox);
        }
    }

    private final static class ChoiceTypeListener
        implements ChangeListener {

        private ChoiceBox choiceBox;

        public ChoiceTypeListener(ChoiceBox choiceBox) {
            this.choiceBox = choiceBox;
        }

        @Override
        public void changed(ObservableValue<? extends Wine$Type> property,
                Wine$Type oldValue, Wine$Type newValue) {
            choiceBox.getSelectionModel().select(newValue);
        }
    }
}

Ouch!

This cell implementation looks intimidating but in fact we simply create 3 text and choice fields for the values we want to edit in the Wine object. Then we set bidirectional binding between each field and the corresponding property of the Wine class. ChoiceBox is the most complex because we can’t bind from the selectedItem property (?), so we have to define a change listener to achieve the same result.

There is nothing else to change, this is purely client code. The persistence will be ensured by the cascading options we have defined on the JPA entity.

Interestingly we don’t have to handle the loading of the collection, Tide will trigger a remote loading of the collection content when the content is first requested, for example when a UI control tried to display the data.

As before, build and run:

git checkout step3

Compare view on GitHub:

cd javafx
mvn clean install
java -jar target/shop-admin-javafx.jar

Step 4: Dirty Checking / Undo

If you have played with the application you may have noticed that using bidirectional bindings leads to a strange behaviour. Even without saving your changes, the local objects are still modified and keep the modifications made by the user. To fix this, we can use the fact that GraniteDS tracks all updates made on the managed entities and is able to easily restore the last known stable state of the objects (usually the last fetch from the server).

We need to inject the local entity manager:

@Inject
private EntityManager entityManager;

And use it to restore the persistent state of the object when the user selects another element without saving:

private void select(Vineyard vineyard) {
    if (vineyard == this.vineyard && this.vineyard != null)
        return;

    if (this.vineyard != null) {
        fieldName.textProperty().unbindBidirectional(this.vineyard.nameProperty());
        fieldAddress.textProperty().unbindBidirectional(this.vineyard.getAddress().addressProperty());
        entityManager.resetEntity(this.vineyard);
    }
    ...
}

We can also enable or disable the ‘Save’ button depending on the fact that the user has modified something or not. Tide provides the DataManager

@Inject
private JavaFXDataManager dataManager;
buttonSave.disableProperty()
    .bind(Bindings.not(dataManager.dirtyProperty()));

If you try this, you will notice that it works fine when modifying existing data but not with newly created elements. This is because these new elements are not known by the entity manager, and thus not tracked by the dirty checking process. To make this work, we have to merge the new entities in the entity manager:

else {
    this.vineyard = new Vineyard();
    this.vineyard.setName("");
    this.vineyard.setAddress(new Address());
    this.vineyard.getAddress().setAddress("");
    entityManager.mergeExternalData(this.vineyard);
}

As before, there is a tag step4 on the git repository.

git checkout step4

Compare view on GitHub:

cd javafx
mvn clean install
java -jar target/shop-admin-javafx.jar

Validation

We can create, edit and search in our database. We would now like to ensure that our data in consistent. The Bean Validation API is our friend and we can use it on both the server JPA entities and on the client data objects.

Going back to the JPA model, we add a few validation annotations, here the Wine class:

@Basic
@Size(min=5, max=100,
    message="The name must contain between {min} and {max} characters")
private String name;

@Basic
@Min(value=1900,
    message="The year must be greater than {value}")
@Past
private Integer year;

@Enumerated(EnumType.STRING)
@NotNull
private Type type;

By adding we ensure that we cannot save incorrect values. However we would also like to notify the user that something went wrong. The brutal way would be to add a special handling of validation error in each and every fault handler of the application. A better way would be to define a global exception handler that will handle all validation faults. Indeed Tide already provides such a thing, and it takes server exceptions and propagates them as events on the faulty property of the target data object. Finally we would have to listen to these events and display some message or trigger some notification to the user. GraniteDS provides a special component, the FormValidator, that will further simplify our work. We will simply have to attach it to the form containing the fields that we want to validate after the entity to validate has been bound:

private FormValidator formValidator = new FormValidator();

...

private void select(Vineyard vineyard) {
    if (vineyard == this.vineyard && this.vineyard != null)
        return;

    formValidator.setForm(null);

    ...

    formValidator.setForm(formVineyard);

    ...
}

Finally we have to define a UI behaviour when a validation event occurs, for example setting a red border on the faulty fields:

formVineyard.addEventHandler(ValidationResultEvent.ANY, new EventHandler() {

    @Override
    public void handle(ValidationResultEvent event) {
        if (event.getEventType() == ValidationResultEvent.INVALID)
            ((Node)event.getTarget()).setStyle("-fx-border-color: red");
        else if (event.getEventType() == ValidationResultEvent.VALID)
            ((Node)event.getTarget()).setStyle("-fx-border-color: null");
    }
});

You could do whatever you want in this handler and apply a more suitable display, for example display the error message.

If you test the application now, that should work fine, but the user is still able to submit the save button even with invalid data. It’s easy to block the remote call:

@FXML
private void save(ActionEvent event) {
    if (!formValidator.validate(this.vineyard))
        return;

    ...
}

Tag step5 on the git repository.

git checkout step5

Compare view on GitHub:

cd javafx
mvn clean install
java -jar target/shop-admin-javafx.jar

Step 6: Security

The application already has a basic security with the login page. If you look how this works, you will find the component Identity which is a gateway between the client and the Spring Security framework.

Just as an exercise, we can add a logout button to our application:

<Button text="Logout" onAction="identity.logout(null)"/>

With a tiny bit of JavaScript, we can call the logout method of identity. As we have defined a change listener on the property loggedIn of identity in the Main class, the current view will be destroyed and replaced by the login screen.

We can also decide in the initialization of the Home controller that only administrators can delete entities:

buttonDelete.disableProperty().bind(Bindings.not(identity.ifAllGranted("ROLE_ADMIN")));

Tag step6 on the git repository.

git checkout step6

Compare view on GitHub:

cd javafx
mvn clean install
java -jar target/shop-admin-javafx.jar

Step 7: Real-time data push

Until now, we have used only one client at a time. We are going to configure GraniteDS to push JPA data updates from the server to all connected clients. We have almost already everything in place, the archetype has setup a complete configuration with Jetty 8 websockets. When deploying on another container, you might need to change the configuration to use the specific websocket support of Tomcat 7+ or GlassFish 3.1.2+, or fallback to simple long-polling with the portable Servlet 3 implementation.

First we need to declare a messaging destination in the server configuration app-config.xml:

<graniteds:messaging-destination id="wineshopTopic"
  no-local="true" session-selector="true"/>

Declare the topic and enable automatic publishing on the Spring Data repository @DataEnabled annotation:

@RemoteDestination
@DataEnabled(topic="wineshopTopic", publish=PublishMode.ON_SUCCESS)
public interface VineyardRepository extends FilterableJpaRepository {
}

Declare a client DataObserver in the Spring configuration and subscribe this topic when the user logs in:

@Bean(initMethod="start", destroyMethod="stop")
public DataObserver wineshopTopic(ServerSession serverSession,
    EntityManager entityManager) {
    return new DataObserver("wineshopTopic", serverSession, entityManager);
}

We listen to the LOGIN and LOGOUT events in the Login controller to subscribe and unsubscribe the topic:

if (ServerSession.LOGIN.equals(event.getType())) {
    wineshopTopic.subscribe();
}
else if (ServerSession.LOGOUT.equals(event.getType())) {
    wineshopTopic.unsubscribe();
}
...

Now you can build the project and run two or more instances of the application in different consoles. Changes made on a client should be propagated to all other subscribed clients.

Tag step7 on the git repository.

git checkout step7

Compare view on GitHub:

cd javafx
mvn clean install
java -jar target/shop-admin-javafx.jar

Conclusion

This tutorial is now finished.

There are still a few more interesting features to show such as conflict detection and resolution but the goal of this tutorial is to show the iterations needed to build a full featured JavaFX application with the help of the GraniteDS JavaFX integration.

JavaFX is still a moving target and some parts of this tutorial might be simplified with future releases, notably as the support for expression bindings in FXML improves.

]]>
/2012/11/26/data-management-tutorial-with-javafx-2-2/feed/ 1
Real-Time Messaging Tutorial with JavaFX 2.2 /2012/11/26/real-time-messaging-tutorial-with-javafx-2-2/ /2012/11/26/real-time-messaging-tutorial-with-javafx-2-2/#comments Mon, 26 Nov 2012 18:08:32 +0000 /?p=1721 Read More

]]>
One of the key features of GraniteDS is its real-time messaging stack, relying on and long-polling. While GraniteDS 3.0.0.M1 introduces an experimental WebSocket support for JavaFX and Flex, this tutorial will focus on a sample JavaFX chat application based on a more conservative setup of Servlet 3.0 asynchronous processing.

For the sake of simplicity, this sample doesn’t require any server side setup: it connects to a Tomcat 7 server, located at the URL, where you will find an existing Flex 4 Chat application.

The architecture of this sample is straightforward: both JavaFX and Flex client applications connect to a GraniteDS asynchronous servlet, which simply dispatch user inputs to all connected users. Both applications rely on the binary AMF3 protocol for exchanging structured data, even if this basic sample only sends and receives Strings.

This tutorial will guide you through the key steps to setup, build and run a Chat JavaFX client application connected to a GraniteDS backend.

1. Requirements

  • (Java client librairies and dependencies)
  • (tested with Juno SR1 64bits, but it should work with prior versions)

2. Setting up the Chat / JavaFX Project

Start Eclipse and create a new Java project named “chat-javafx” (accept all default settings).

Create a new folder in the project and name it “libs”. From the granite 3.0.0.M1 distribution, add the following jars, located in the libraries/java-client directory:

  • granite-client.jar
  • granite-java-client.jar
  • commons-codec-1.6.jar
  • commons-logging-1.1.1.jar
  • httpasyncclient-4.0-beta3.jar
  • httpclient-4.2.1.jar
  • httpcore-4.2.2.jar
  • httpcore-nio-4.2.2.jar

Even if we are creating a GraniteDS / JavaFX project here, we don’t need to include granite-javafx-client.jar, which is only required for advanced data management. The last six librairies come from the project and are required for HTTP asynchronous calls to the GraniteDS backend.

Now, add those libraries to the build path of your project: select all height libraries, right click on them, then select “Build Path” -> “Add to Build Path”.

Finally, you need to complete the build path of your project by adding the jfxrt.jar library, which is not part of the JavaSE-1.7 installed JRE under Eclipse: right click on “Referenced Libraries” in the project and select “Build Path” -> “Configure Build Path…”. Click on the “Add External JARs…” button and locate the jfxrt.jar in your JavaSE-1.7 installation (under jre/lib). Select it and click on “Ok”.

Your project is now fully configured for that basic GraniteDS / JavaFX Chat project and should look as follow:

Make sure the selected JRE System Library is JavaSE-1.7 and that jfxrt.jar is in your References Libraries (its location can of course differ from the one on the picture, depending on your platform).

3. Writing the Chat class

Create a new HelloWorld class in the “src” source directory with its package set to “org.granite.client.examples.helloworld”. Then, copy-paste the following code:

package org.granite.examples.javafx.chat;



import java.net.URI;

import java.util.concurrent.ExecutionException;

import java.util.concurrent.TimeoutException;



import javafx.application.Application;

import javafx.event.EventHandler;

import javafx.geometry.Insets;

import javafx.scene.Group;

import javafx.scene.Scene;

import javafx.scene.control.TextArea;

import javafx.scene.control.TextField;

import javafx.scene.input.KeyEvent;

import javafx.scene.layout.VBox;

import javafx.stage.Stage;



import org.granite.client.messaging.Consumer;

import org.granite.client.messaging.Producer;

import org.granite.client.messaging.TopicMessageListener;

import org.granite.client.messaging.channel.MessagingChannel;

import org.granite.client.messaging.channel.amf.AMFMessagingChannel;

import org.granite.client.messaging.events.TopicMessageEvent;

import org.granite.client.messaging.transport.HTTPTransport;

import org.granite.client.messaging.transport.apache.ApacheAsyncTransport;



public class Chat extends Application {



    private HTTPTransport transport;

    private MessagingChannel channel;

    private Consumer consumer;

    private Producer producer;

    

    private TextField input;

    private TextArea output;

    

    public static void main(String[] args) {

        Application.launch(args);

    }



    @Override

    public void init() throws Exception {

        super.init();



        transport = new ApacheAsyncTransport();

        transport.start();

        

        URI uri = new URI("http://demo.graniteds.org/chat/gravity/amf");

        channel = new AMFMessagingChannel(transport, "graniteamf", uri);

        

        consumer = new Consumer(channel, "gravity", "discussion");

        producer = new Producer(channel, "gravity", "discussion");

    }



    @Override

    public void start(Stage stage) {

        

        final String username = System.getProperty("user.name");

        

        stage.setTitle("Chat GraniteDS/JavaFX");

        stage.setResizable(false);



        Group root = new Group();

        stage.setScene(new Scene(root));

        

        VBox box = new VBox();

        box.setPadding(new Insets(8.0));

        box.setSpacing(8.0);

        root.getChildren().add(box);

        

        output = new TextArea();

        box.getChildren().add(output);

        output.setEditable(false);

        output.setStyle("-fx-border-style: none");

        output.setFocusTraversable(false);

        

        input = new TextField();

        box.getChildren().add(input);

        input.setOnKeyTyped(new EventHandler<KeyEvent>() {

            

            private int index = 1;

            

            @Override

            public void handle(KeyEvent event) {

                if ("r".equals(event.getCharacter()) || "n".equals(event.getCharacter())) {

                    producer.publish("[" + username + " #" + (index++) + "] " + input.getText().trim());

                    input.setText("");

                }

            }

        });



        stage.show();

        input.requestFocus();



        consumer.addMessageListener(new TopicMessageListener() {

            

            @Override

            public void onMessage(TopicMessageEvent event) {

                output.appendText((String)event.getData() + "n");

            }

        });

        try {

            consumer.subscribe().get();

        } catch (InterruptedException | ExecutionException | TimeoutException e) {

            throw new RuntimeException(e);

        }



        producer.publish("[" + username + " has just connected]");

    }



    @Override

    public void stop() throws Exception {

        if (consumer != null && consumer.isSubscribed())

            consumer.unsubscribe().get();

        

        transport.stop();



        super.stop();

    }

}

The Chat project is now ready and should compile without any issues.

3. Running the Application

Right click on the “Chat.java” class in your Eclipse Package Explorer and select “Run As” -> “Java Application”. The application should show up, saying that you are connected as “JavaFX” and letting you entering some input, followed by the <enter> or <return> key:

Chatting with yourself isn’t very exiting so you had better to find a friend running the application at the same time. Note that the server being public you could end up chatting with other testers from different locations.

4. Understanding the Highlighted Code

After having (a lot) of fun with the application, it’s time to understand what is going on under the hood.

Let’s look at the first part of the highlighted code (lines 45-52):

transport = new ApacheAsyncTransport();

transport.start();



URI uri = new URI("http://demo.graniteds.org/chat/gravity/amf");

channel = new AMFMessagingChannel(transport, "graniteamf", uri);



consumer = new Consumer(channel, "gravity", "discussion");

producer = new Producer(channel, "gravity", "discussion");

This part of code is executed at initialization time and proceed as follow:

  1. A new ApacheAsyncTransport instance is created and stored into a class variable of the HttpTransport type (which is an interface). This transport is then started and ready to send asynchronous HTTP requests.
  2. A new AMFMessagingChannel instance is created and bound to the transport, a channel id (“graniteamf”) and the URI of a GraniteDS servlet handling AMF3 asynchronous requests. You’ll need to have a closer look at the server-side to fully understand the meaning of these settings (see below).
  3. Then, a Consumer instance is created, bound to the channel, a destination id (“gravity”) which identifies the real-time messaging service on the server-side and a topic (“discussion”).
  4. Finally, a Producer is created, bound to the same channel, destination id and topic.

What we get at the end of this initialization is basically an object (producer) from which we can publish messages in the “discussion” topic and another object (consumer) which will receive all messages published on that same topic.

The second part of the highlighted code is responsible of publishing the user input:

producer.publish("[" + username + " #" + (index++) + "] " + input.getText().trim());

For a given username (say “johndoe”) and input (say “Bla bla”), the producer will publish the message “[johndoe #1] Bla bla”, the index (“#1″) being an incremental counter of the successive messages sent.

The third part of the highlighted code is setting up the consumer handler, subscription to the topic and publishing an initial message:

consumer.addMessageListener(new TopicMessageListener() {

            

    @Override

    public void onMessage(TopicMessageEvent event) {

        output.appendText((String)event.getData() + "n");

    }

});

try {

    consumer.subscribe().get();

} catch (InterruptedException | ExecutionException | TimeoutException e) {

    throw new RuntimeException(e);

}



producer.publish("[" + username + " has just connected]");

A TopicMessageListener is attached to the consumer and displays received messages in the text area at the top of the application. Then, the consumer subscribes to the “discussion” topic, waiting for the subscription completion (consumer.subscribe().get()): the subscribe method is asynchronous and returns a Future which can be used to wait for an acknowledgment message from the server.

Finally, a first message is published by the producer, announcing a new connection to the chat topic (eg. “[johndoe has just connected]“).

The final part of the highlighted code unsubscribes the consumer and stops the transport when the application is closed:

if (consumer != null && consumer.isSubscribed())

    consumer.unsubscribe().get();

        

transport.stop();

Again, the unsubscribe call is asynchronous and the get() call is blocking until the consumer receives an acknowledgment from the server.

All resources allocated by the transport are then released, in particular the underlying thread pool managing asynchronous requests: failing to call the transport stop method can lead to improper thread closing, leaving the application in a kind of daemon state.

4. A Look at the Server Code

In the “samples” directory of the GraniteDS 3.0.0.M1 distribution, you will find a file named “sample_projects.zip”. You can unzip it at location of your choice or import it as an existing projets archive under Eclipse.

Let’s look at the file that explains the above configuration of the Consumer and Producer (chat/war/WEB-INF/flex/services-config.xml):

<services-config>



    <services>

        <service id="messaging-service"

            class="flex.messaging.services.MessagingService"

            messageTypes="flex.messaging.messages.AsyncMessage">

            <adapters>

                <adapter-definition id="default" class="org.granite.gravity.adapters.SimpleServiceAdapter" default="true"/>

            </adapters>



            <destination id="gravity">

                <channels>

                    <channel ref="gravityamf"/>

                </channels>

            </destination>

        </service>

    </services>



    <channels>

        <channel-definition id="gravityamf" class="org.granite.gravity.channels.GravityChannel">

            <endpoint

                uri="http://{server.name}:{server.port}/{context.root}/gravity/amf"

                class="flex.messaging.endpoints.AMFEndpoint"/>

        </channel-definition>

    </channels>



</services-config>

Let’s start with the channel definition: an asynchronous real-time channel identified by “gravityamf” is bound to an uri which resolves to “http://demo.graniteds.org/chat/gravity/amf”. If you look at the web.xml file, this is where the GraniteDS (Gravity) servlet is handling incoming AMF messages. The configuration of the AMFMessagingChannel in the JavaFX Chat class reflects these settings.

Then, jump to the beginning of the file and look at the service definition: a destination “gravity” is declared, bound to the “gravityamf” channel. This explains the configuration of our Consumer and Producer as well.

The last thing to understand is that we don’t need to declare any predefined topic on the server-side: Gravity, the real-time messaging engine of GraniteDS, has a built-in support for basic topic creation and handling. That’s why we can define the “discussion” topic in the client code, without any further configuration on the server. You can change it to whatever you want (say “my-private-discussion”) and initiate a private chat on this new topic.

]]>
/2012/11/26/real-time-messaging-tutorial-with-javafx-2-2/feed/ 2
Hello World Tutorial with JavaFX 2.2 /2012/11/26/hello-world-tutorial-with-javafx-2-2/ /2012/11/26/hello-world-tutorial-with-javafx-2-2/#comments Mon, 26 Nov 2012 18:07:17 +0000 /?p=1681 Read More

]]>
Here is an over-classical and basic Hello World sample application that introduces the new support for Java and JavaFX in GraniteDS 3.0.0.M1. For the sake of simplicity, this sample doesn’t require any server side setup: it connects to a Tomcat 7 server, located at the URL, where you will find an existing Flex 4 sample, working with a very basic POJO service.

The architecture of this sample is straightforward: both JavaFx and Flex client applications connect to a GraniteDS POJO service, which simply echoes back your input, surrounded by “Hello ” and “!”. Both applications rely on the binary AMF3 protocol for exchanging structured data, even if this basic sample only sends and receives Strings.

This tutorial will guide you through the key steps to setup, build and run a HelloWorld JavaFX client application connected to a GraniteDS backend.

1. Requirements

  • (Java client librairies and dependencies)
  • (tested with Juno SR1 64bits, but it should work with prior versions)

2. Setting up the HelloWorld / JavaFX Project

Start Eclipse and create a new Java project named “helloworld-javafx” (accept all default settings).

Create a new folder in the project and name it “libs”. From the granite 3.0.0.M1 distribution, add the following jars, located in the libraries/java-client directory:

  • granite-client.jar
  • granite-java-client.jar
  • commons-codec-1.6.jar
  • commons-logging-1.1.1.jar
  • httpasyncclient-4.0-beta3.jar
  • httpclient-4.2.1.jar
  • httpcore-4.2.2.jar
  • httpcore-nio-4.2.2.jar

Even if we are creating a GraniteDS / JavaFX project here, we don’t need to include granite-javafx-client.jar, which is only required for advanced data management. The last six librairies come from the project and are required for HTTP asynchronous calls to the GraniteDS backend.

Now, add those libraries to the build path of your project: select all height libraries, right click on them, then select “Build Path” -> “Add to Build Path”.

Finally, you need to complete the build path of your project by adding the jfxrt.jar library, which is not part of the JavaSE-1.7 installed JRE under Eclipse: right click on “Referenced Libraries” in the project and select “Build Path” -> “Configure Build Path…”. Click on the “Add External JARs…” button and locate the jfxrt.jar in your JavaSE-1.7 installation (under jre/lib). Select it and click on “Ok”.

Your project is now fully configured for that basic GraniteDS / JavaFX Hello World project and should look as follow:

Make sure the selected JRE System Library is JavaSE-1.7 and that jfxrt.jar is in your References Libraries (its location can of course differ from the one on the picture, depending on your platform).

3. Writing the HelloWorld class

Create a new HelloWorld class in the “src” source directory with its package set to “org.granite.client.examples.helloworld”. Then, copy-paste the following code:

package org.granite.client.examples.helloworld;



import java.net.URI;



import javafx.application.Application;

import javafx.event.EventHandler;

import javafx.geometry.Insets;

import javafx.scene.Group;

import javafx.scene.Scene;

import javafx.scene.control.Label;

import javafx.scene.control.TextArea;

import javafx.scene.control.TextField;

import javafx.scene.input.KeyEvent;

import javafx.scene.layout.VBox;

import javafx.stage.Stage;



import org.granite.client.messaging.RemoteService;

import org.granite.client.messaging.ResponseListener;

import org.granite.client.messaging.ResultIssuesResponseListener;

import org.granite.client.messaging.channel.RemotingChannel;

import org.granite.client.messaging.channel.amf.AMFRemotingChannel;

import org.granite.client.messaging.events.IssueEvent;

import org.granite.client.messaging.events.ResultEvent;

import org.granite.client.messaging.transport.HTTPTransport;

import org.granite.client.messaging.transport.apache.ApacheAsyncTransport;



public class HelloWorld extends Application {



    private HTTPTransport transport;

    private RemoteService remoteService;



    private TextField input;

    private TextArea output;



    public static void main(String[] args) {

        Application.launch(args);

    }



    @Override

    public void init() throws Exception {

        super.init();



        transport = new ApacheAsyncTransport();

        transport.start();



        URI uri = new URI("http://demo.graniteds.org/helloworld/graniteamf/amf");

        RemotingChannel channel = new AMFRemotingChannel(transport, "graniteamf", uri);



        remoteService = new RemoteService(channel, "helloWorldService");

    }



    @Override

    public void start(Stage stage) {



        stage.setTitle("HelloWorld GraniteDS/JavaFX");

        stage.setResizable(false);



        Group root = new Group();

        stage.setScene(new Scene(root));



        VBox box = new VBox();

        root.getChildren().add(box);

        box.setPadding(new Insets(8.0));

        box.setSpacing(8.0);



        output = new TextArea();

        box.getChildren().add(output);

        output.setEditable(false);



        box.getChildren().add(new Label("Enter your name and type :"));



        input = new TextField();

        box.getChildren().add(input);

        input.setOnKeyTyped(new EventHandler() {



            private ResponseListener listener = new ResultIssuesResponseListener() {



                @Override

                public void onResult(ResultEvent event) {

                    output.appendText((String)event.getResult() + "n");

                }



                @Override

                public void onIssue(IssueEvent event) {

                    output.appendText(event.toString() + "n");

                }

            };



            @Override

            public void handle(KeyEvent event) {

                if (!event.getCharacter().isEmpty()) {

                    char c = event.getCharacter().charAt(0);

                    if (c == 'n' || c == 'r') {

                        remoteService.newInvocation("sayHello", input.getText()).addListener(listener).invoke();

                        input.setText("");

                    }

                }

            }

        });



        stage.show();



        input.requestFocus();

    }



    @Override

    public void stop() throws Exception {

        transport.stop();



        super.stop();

    }

}

The HelloWorld project is now ready and should compile without any issues.

3. Running the Application

Right click on the “HelloWorld.java” class in your Eclipse Package Explorer and select “Run As” -> “Java Application”. The application should show up, letting you entering your name and saying in return, as expected, Hello:

As explained in the label above the text field, just enter some text and press the enter (or return) key on your keyboard.

4. Understanding the Highlighted Code

After having (a lot) of fun with the application, it’s time to understand what is going on under the hood.

Let’s look at the first part of the highlighted code (lines 43-49):

transport = new ApacheAsyncTransport();

transport.start();



URI uri = new URI("http://demo.graniteds.org/helloworld/graniteamf/amf");

RemotingChannel channel = new AMFRemotingChannel(transport, "graniteamf", uri);



remoteService = new RemoteService(channel, "helloWorldService");

This part of code is executed at initialization time and proceed as follow:

  1. A new ApacheAsyncTransport instance is created and stored into a class variable of the HttpTransport type (which is an interface). This transport is then started and ready to send asynchronous HTTP requests.
  2. A new AMFRemotingChannel instance is created and bound to the transport, a channel id (“graniteamf”) and the URI of a GraniteDS servlet handling AMF3 requests. You’ll need to have a closer look at the server-side to fully understand the meaning of these settings (see below).
  3. Finally, a RemoteService instance is created, bound to the channel and a destination id (“helloWorldService”) which identifies the Java POJO service on the server-side. Again, a closer look at the server code is required, see below.

What we get at the end of this initialization is basically an object (remoteService) from which we can call methods of a remote Java service (the helloWorldService).

The second part of the highlighted code is a handler for server responses:

private ResponseListener listener = new ResultIssuesResponseListener() {



    @Override

    public void onResult(ResultEvent event) {

        output.appendText((String)event.getResult() + "n");

    }



    @Override

    public void onIssue(IssueEvent event) {

        output.appendText(event.toString() + "n");

    }

};

The two methods of the ResultIssuesResponseListener interface deal respectively with successful answers (the server responds with “Hello ” + your input + “!”) and issues (something went wrong). Both display their output in the text area of the application.

The third part of the highlighted code is the server request:

remoteService.newInvocation("sayHello", input.getText())

             .addListener(listener)

             .invoke();

The remoteService, bound to the hello world POJO service on the server, creates a new invocation of the method “sayHello”, with the user input as parameter (newInvocation(“sayHello”, input.getText())). Then, the just created handler (see above) is set as the listener of the server response (addListener(listener)) and the call is placed (invoke()). This happens each time the user input a new text.

The final part of the highlighted code stops the transport when the application is closed:

transport.stop();

All resources allocated by the transport are then released, in particular the underlying thread pool managing asynchronous requests: failing to call the transport stop method can lead to improper thread closing, leaving the application in a kind of daemon state.

4. A Look at the Server Code

In the “samples” directory of the GraniteDS 3.0.0.M1 distribution, you will find a file named “sample_projects.zip”. You can unzip it at location of your choice or import it as an existing projets archive under Eclipse.

Let’s look at the file that explains the configuration of the RemotingChannel and RemotingService (helloworld/war/WEB-INF/flex/services-config.xml):

<services-config>



    <services>

        <service id="granite-service"

            class="flex.messaging.services.RemotingService"

            messageTypes="flex.messaging.messages.RemotingMessage">

            <destination id="helloWorldService">

                <channels>

                    <channel ref="graniteamf"/>

                </channels>

                <properties>

                    <scope>application</scope>

                    <source>org.test.HelloWorldService</source>

                </properties>

            </destination>

        </service>

    </services>



    <channels>

        <channel-definition id="graniteamf" class="mx.messaging.channels.AMFChannel">

            <endpoint

                uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"

                class="flex.messaging.endpoints.AMFEndpoint"/>

        </channel-definition>

    </channels>



</services-config>

Let’s start with the channel definition: a channel identified by “graniteamf” is bound to an uri which resolves to “http://demo.graniteds.org/helloworld/graniteamf/amf”. If you look at the web.xml file, this is where the main GraniteDS filter and servlet are handling incoming AMF requests. The configuration of the AMFRemotingChannel in the JavaFX HelloWorld class reflects these settings.

Then, jump to the beginning of the file and look at the service definition: the destination “helloWorldService”, which uses the “graniteamf” channel, is bound to the org.test.HelloWorldService POJO. This explains the configuration of our RemotingService as well.

Finally, let’s have a look at the HelloWorldService class (helloworld/java/org/test/HelloWorldService.java):

package org.test;



public class HelloWorldService {



    public String sayHello(String name) {

        return "Hello " + name + "!";

    }

}

The invocation of the service (ie: remoteService.newInvocation(“sayHello”, input.getText())) is now all obvious: the method “sayHello” is called with a parameter set to the current user input.

]]>
/2012/11/26/hello-world-tutorial-with-javafx-2-2/feed/ 0