terça-feira, 7 de janeiro de 2014

JavaFX CRUD: The Client Side

Beta post
In my last post I showed a JEE application to make CRUD operation on a domain object called Framework, which represents an application framework. In this post we will show the client part, the technologies that were used and how simple is to create JavaFX applications using FXML and pure Java.

The view

I had the honor of being one of the first "bloggers" to test JavaFX Scene Builder. Using this tool you can drag and drop interface elements and a XML will be generated and it has the .fxml extension.
This FXML can be loaded in a JavaFX application and the view elements can be inject in a controller class using the @FXML annotation. Screenshots from the application are in a previous post.
Then, in Java side, we have a class called CrudframeworksPresenter. This class is linked with the FXML and it the presenter, which updates the view and handle the CRUD actions. See the FXML source code first line where I refer to the controller class.
I tell the controller in CrudframeworksPresenter and then I can start injecting elements using the FXML annotation  on the controller declared attributes. In our case, I inject everything that I'll modify according to the application logic and flow. Notice that the attribute name is the same of the element declared on FXML:

 
public class CrudframeworksPresenter implements Initializable {

        //Injected fields
        @FXML
        StackPane pnlRoot;
        // Components from the View Pane
        @FXML
        Label message;
        @FXML
        AnchorPane pnlTable;
        @FXML
        Label lblStatus;
        @FXML
        TextField txtFilter;
        @FXML
        RadioButton rbName;
        @FXML
        RadioButton rbPlatform;
        @FXML
        TableView tblFrameworks;
        @FXML
        AnchorPane viewPane;
        @FXML
        TableColumn columnLastRelease;

        // Components from the Edit Pane
        @FXML
        TitledPane titlePaneFramework;
        @FXML
        TextField txtName;
        @FXML
        TextField txtCreator;
        @FXML
        TextField txtReleaseDate;
        @FXML
        TextField txtHomePage;
        @FXML
        TextField txtPlatform;
        @FXML
        TextField txtCurrentVersion;
        @FXML
        TextArea txtDescription;
        @FXML
        Button btnPnlAction;
        @FXML
        AnchorPane editPane;
        @FXML
        Button btnCancelEditionMode;
        @FXML
        Label lblValidationError;
//....


From the controller I also handle the view actions. I set the actions on the FXML and a corresponding method was created on the controller class. The actions are for the buttons, the filter field and for the context menu.

FXML declared actions:
....
<MenuItem mnemonicParsing="false" onAction="#modifyFrameworkAction" text="Modify" />
<MenuItem mnemonicParsing="false" onAction="#removeFrameworkAction" text="Remove" />
...
<TextField fx:id="txtFilter" onAction="#filterTable" prefWidth="200.0" />
...
<Button mnemonicParsing="false" onAction="#refreshTable" text="Refresh" />
<Button mnemonicParsing="false" onAction="#addFrameworkAction" text="Add" />


Methods for the actions:
 
public void modifyFrameworkAction() {
        showFrameworkPane(Mode.UPDATE);
}

public void addFrameworkAction() {
        showFrameworkPane(Mode.NEW);
}
//...
public void filterTable() {
//...
}
public void removeFrameworkAction() {
//...
}

Communicating with the server

As said in my previous post, the server exposes the CRUD operations(Create, Retrieve, Update and Delete) in a RESTful interface. To communicate with the server in our application, we simple need to use a REST client API that will perform calls to the server and help us to make a high level programming without concerning about object parsing, managing HTTP connections and handle all the possible response from the server.
For this purpose I chose RESTEasy 2.x client framework. However, we have a new API, which was defined in JSR 339: The JAX-RS 2 Client API (which is in RESTeasy 3.x). I kept the project using the RESTeasy 2 client framework, but there's an open request to move to JAX-RS 2 Client API.

The class that we perform the operation with the server is FrameworkClientService. It will simple produce HTTP client requests objects for the given parameters, so for each Java method  that should perform the crud operation (add, remove, update, get and getAll).
Notice that's not the best way to make our *Service.java class. The operations should be defined in a Java Interface and then we should create implementations of this interface according how we persisted our object. For example, in this app we access a REST Web Service, but it could be a local DB or the filesystem, making it an interface and injecting the implementation we want, makes the system more flexible. Anyway, here's the code of our Service:

 
import java.io.FileInputStream;
import java.util.List;
import java.util.Properties;

import javax.management.RuntimeErrorException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriBuilder;

import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.jboss.resteasy.client.ClientRequest;
import org.jboss.resteasy.client.ClientResponse;
import org.jboss.resteasy.util.GenericType;
import org.jugvale.crudframeworks.client.business.Framework;

/**
 * REST Client for the Framework business class
 * 
 * @author william
 * 
 */
public class FrameworkClientService {

        private final String BASE_URI;

        public FrameworkClientService() {
                Properties p = new Properties();
                try {
                        p.load(new FileInputStream(getClass().getResource(
                                        "/conf/crudframeworks.properties").getFile()));
                } catch (Exception e) {
                        e.printStackTrace();
                        System.err.println("ERROR LOADING crudframeworks.properties file");
                }
                BASE_URI = p.getProperty("host");
        }

        public void add(Framework framework) {
                doRequest(createRequest().body(MediaType.APPLICATION_JSON, framework), HttpPost.METHOD_NAME, null);
        }

        public Framework get(int id) {
                return (Framework) doRequest(createRequest(id), HttpGet.METHOD_NAME,
                                new GenericType() {
                                });
        }

        public void remove(int id) {
                doRequest(createRequest(id), HttpDelete.METHOD_NAME, null);
        }

        public void update(Framework framework) {
                doRequest(createRequest(framework.getId()).body(
                                MediaType.APPLICATION_JSON, framework), HttpPut.METHOD_NAME, null);
        }

        @SuppressWarnings("unchecked")
        public List getAll() {
                return (List) doRequest(createRequest(), HttpGet.METHOD_NAME,
                                new GenericType>() {
                                });
        }

        private Object doRequest(ClientRequest cr, String httpMethod,
                        GenericType returnType) {
                ClientResponse r = null;
                Object serverResponseBody = null;
                try {
                        System.out.println("PERFORM A "+ httpMethod + " ON " + cr.getUri());
                        r = cr.httpMethod(httpMethod);
                        // ??
                        int status = r.getStatus();
                        if ( status >= 500) {
                                handleError("Server is having a bad time with this request, try again later...");
                        } else if (status == 404) {
                                handleError("Framework with ID "
                                                + cr.getPathParameters().get("id") + " not found.");
                                // if it's not a success code
                        }else if (r.getStatus() >= 300){
                                handleError("The server responded with an unexpected status: "+ r.getStatus());
                        }
                } catch (Exception e) {
                        e.printStackTrace();
                        handleError("Unknown error: " + e.getMessage());
                }
                if (returnType != null)
                        serverResponseBody = r.getEntity(returnType);
                cr.clear();
                return serverResponseBody;
        }

        private ClientRequest createRequest(int id) {
                return new ClientRequest(UriBuilder.fromPath(BASE_URI).path(String.valueOf(id)).build().toString());
        }
        private ClientRequest createRequest() {
                        return new ClientRequest(BASE_URI);
        }

        private void handleError(String message) {
                throw new RuntimeErrorException(new Error(message));
        }
}

Application logic

Our logic is simple. We take data from the server and update the view; We handle user actions and send requests to the server, with the server response we update the view again. It's something close to what they call model-view-presenter MVP architecture design pattern. MVP in JavaFX is simplified by the Afterburner.fx framework.
I started the app using on Afterburner, but then I removed it because it uses Java 8 and I was having annoying small issues which were distracting from the main focus: show an application that makes remote communication with a server. Afterburner.fx is a nice and simple framework, I recommend you for new projects.
Our application logic is in class CrudframeworksPresenter and it uses the FrameworkClientService class, which is responsible to "talk" to the server. This class will modify the application screen according user interactions and it will also fill the app with new data coming from the server.
Before we send information to the server, we need to validate it. The application validation is manual, I don't use any framework to do this, see:

 private boolean setFrameworkFieldsAndValidate(Framework f) {
        if (f == null) {
                f = new Framework();
        }
        String name = txtName.getText();
        String currentVersion = txtCurrentVersion.getText();
        String homePage = txtHomePage.getText();
        String creator = txtCreator.getText();
        String lastReleaseDate = txtReleaseDate.getText();
        String description = txtDescription.getText();
        String platform = txtPlatform.getText();

        if (name.isEmpty() || name.length() < 3) {
                validationError("Name lenght should be greater than 3");
        } else if (currentVersion.isEmpty()) {
                validationError("You need to inform the framework current version");
        } else if (creator.isEmpty()) {
                validationError("Creator is obligatory");
        } else if (platform.isEmpty()) {
                validationError("Platform is obligatory");
        }
        // passed all validations...
        else {
                try {
                        f.setDescription(description);
                        f.setHomePage(homePage);
                        f.setName(name);
                        f.setCreator(creator);
                        f.setPlatform(platform);
                        // can't fail the following fields, if so, it won't validate...
                        f.setCurrentVersion(Double.valueOf(txtCurrentVersion.getText()));
                        // date isn't obligatory.. but user needs to set a valid date.
                        if (!lastReleaseDate.isEmpty()) {
                                Date releaseDate = dataFormatter.parse(lastReleaseDate);
                                f.setLastReleaseDate(releaseDate);
                        }
                        return true;
                } catch (ParseException e) {
                        validationError("Check Last Release Date. Use format "
                                        + dataFormatter.toPattern());
                } catch (NumberFormatException e) {
                        validationError("Invalid version");
                }
        }
        return false;
}

We also have a small method to update the view when we have validation error and another label to show the user the status of the communication. These methods are called several times in the code.

private void validationError(String value) {
    lblValidationError.setText(value);
}
 private void status(String status) {
    lblStatus.setText(status);
}

The application has what I call mode. User can be in edit mode or in add mode and I can decide what action the buttons will have. The mode is an enum and it changes according user action. When the user clicks to add a new Framework, we use this action to show the Framework panel in new mode, when he wants to modify some existing framework, we open it in update mode and retrieve the selected framework to fill the fields with the framework values. The update action is called from the context menu and the new framework from a button:

public void modifyFrameworkAction() {
        showFrameworkPane(Mode.UPDATE);
}

public void addFrameworkAction() {
        showFrameworkPane(Mode.NEW);
}
//...
enum Mode {
 UPDATE, NEW
}

Building the Client part

The project that contains the client part is X. Use git clone feature to have this project locally:

$ git clone https://github.com/jesuino/crud-frameworks.git

To build it, modify the dependency javafx in pom.xml file to set the path of the jfxrt.jar library in your installation.
Now the project should be "buildable". Notice that it will run tests agains a localhost running JBoss. If you don't want to do this, tell maven to skip the tests when building it. You can also import the project into your IDE and run it from there.

Improvements
Here's a list of improvements that could be done in this project. I created an issue for each of the possible improvements:
* Use JAX-RS 2 client API

Deploying to Openshift

The server part is on Openshift and you can reach the server with any client. The app is in URL http://crud-fxapps.rhcloud.com/crud-frameworks-rest/ and the REST API endpoint is http://crud-fxapps.rhcloud.com/crud-frameworks-rest/rs/frameworks

Deploy the server part on Openshift wasn't hard at all. It allow us to quickly send our application to the cloud without too much complication.

Conclusion(or "why are you creating this?")

The main duty of this post and the previous two posts was to show a JavaFX application that can interact to a server that uses JavaEE. It was used in some seminars when I showed the audience that it's possible to have modern quick application development only using Java and JavaEE. Of course, the application could be more simple, but with this I could target specific questions that I usually hear from the JavaFX Brazilian Community. Then I can use this project as an example.
I had more plans for it, but Java 8 is coming and I have some stuff I need to focus, so I decided to blog what I have done so far to keep moving one.

Um comentário: