Sunday, May 15, 2016

Uberfire - The Unanswered Questions: Part 5

Service!

In Part 4 I promised to present a better solution to the model serialization problem. In Part 5 I will introduce Errai RPC (Remote Procedure Calls) and show you a service implementation for our UFTasks tutorial.

Errai supports a very easy to use RPC layer for implementing services that can be called from either the client or server side. This requires three components:
  1. a service interface definition
  2. a service implementation
  3. if model objects will be passed to/from the service, they need to be annotated with @Portable

Service Interface

The service interface definition is very simple - it just requires a @Remote annotation:

import org.jboss.errai.bus.server.annotations.Remote;
import org.uberfire.component.model.TasksRoot;

@Remote
public interface UFTasksService {
    TasksRoot load(String userId);
    String save(TasksRoot tasksRoot, String userId);
}

Here we have defined two methods, load and save - their purpose should be obious. Note that since this service will be passing a TasksRoot model object back and forth, the TasksRoot class must be annotated with @Portable (see below).

Service Implementation

This is our implementation class for the service. Note that Errai already provides a JSON marshalling class - how convenient!

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import org.jboss.errai.bus.server.annotations.Service;
import org.jboss.errai.marshalling.client.Marshalling;
import org.uberfire.backend.vfs.Path;
import org.uberfire.backend.vfs.PathFactory;
import org.uberfire.backend.vfs.VFSService;
import org.uberfire.component.model.TasksRoot;
import org.uberfire.component.service.UFTasksService;

@Service
@ApplicationScoped
public class UFTasksServiceImpl implements UFTasksService {
    private final static String FILENAME = "tasks.json";
    private final static String DEFAULT_URI = "default://uftasks";

    @Inject
    protected VFSService vfsServices;

    @Override
    public TasksRoot load(String userId) {
        String uri = DEFAULT_URI + "/" + userId + "/" + FILENAME;
        Path path = PathFactory.newPath(FILENAME, uri);

        String content = vfsServices.readAllString(path);
        TasksRoot tasksRoot = Marshalling.fromJSON(content, TasksRoot.class);
        return tasksRoot;
    }

    @Override
    public String save(TasksRoot tasksRoot, String userId) {
        String content = Marshalling.toJSON(tasksRoot);
        String uri = DEFAULT_URI + "/" + userId + "/" + FILENAME;
        Path path = PathFactory.newPath(FILENAME, uri);

        path = vfsServices.write(path, content);
        if (path!=null)
            return path.getFileName();
        return null;
    }
}

Notice here that since this service is being executed on the server side, the VFSService is a "peer" and can be called directly instead of having to use a Callback. Recall that, on the client side the client had to invoke VFSService asynchronously because information was sent over the wire from client to server and then back to client.

Model Objects

Finally, our model objects need to be annotated with @Portable. This tells Errai that the object will need to be serialized so that it can be sent over the wire between client and server.

import java.util.ArrayList;
import java.util.List;

import org.jboss.errai.common.client.api.annotations.Portable;

@Portable
public class TasksRoot {
    private List projects = new ArrayList();
    
    public TasksRoot() {
    }
    
    public List getProjects() {
        return projects;
    }
}

Errai uses Java reflection to traverse the object's class definition. This means that any class fields that are not primitive types, or simple java List types must also be annotated as @Portable - in our case the Project, Folder, Task and TreeNode class definitions.

Errai does include an extensive marshalling framework, and we could provide our own custom serialization and deserialization routines instead of relying on Java reflection, but that's a more advanced topic that I may cover later.

Client-side Changes

Now that we have our UFTasks service, we can use it on the client-side. Recall that the ProjectsPresenter did all of the task loading and saving before. This code now simply becomes:

@ApplicationScoped
@WorkbenchScreen(identifier = "ProjectsPresenter")
public class ProjectsPresenter {

    ....

    @Inject
    Caller<UFTasksService> ufTasksService;

    ....

    private void loadTasksRoot() {
        ufTasksService.call(new RemoteCallback<TasksRoot>() {
            @Override
            public void callback(final TasksRoot response) {
                if (response!=null)
                    tasksRoot = response;
                else 
                    GWT.log("UFTasksService is unable to load tasks file");
                updateView();
            }
        }).load(user.getIdentifier());
    }
    
    private void saveTasksRoot() {
        ufTasksService.call(new RemoteCallback<String>() {
            @Override
            public void callback(final String response) {
                GWT.log("Write Response: " + response);
            }
        }).save(tasksRoot, user.getIdentifier());
    }

Everything else stays the same.


Reorganizing UFTasks

If you have read through the Uberfire documentation, you should already be familiar with the layout of the Uberfire Archetype. To summarize, the structure looks something like this:
  • bom: "bill of materials" of the archetype. It defines the versions of all the artifacts that will be created in the library
  • parent-with-dependencies: declares all dependencies and versions of the archetype.
  • component: the uberfire component project containing server-side, client-side and common components.
  • showcase: uberfire showcase directory containing the web app and distribution-wars.
Following this archetype, it's probably a good idea to reorganize some of our classes. Here's the new project structure:


Notice that the model objects have been moved to the components project since they are now considered "shared" between client and server. The same applies to the UFTasksService interface, since this is used by both client and server code. The UFTasksServiceImpl implementation however now resides on the server side.

If you are using Eclipse, this is simply a refactoring operation and all references to these classes should be updated automatically. If you get confused you can always grab the latest code from here.

A Closer Look at Errai Marshalling

The first thing you'll notice when running this version of UFTasks is that the original JSON file developed in Part 4 is no longer compatible with this version of the app. That's because, as I mentioned earlier, Errai uses Java reflection to figure out the structure of the model object being serialized. What you'll see after creating a new uftasks.json file is something like this:

{
    "^EncodedType": "org.uberfire.component.model.TasksRoot",
    "^ObjectID": "1",
    "projects": {
        "^EncodedType": "java.util.ArrayList",
        "^ObjectID": "2",
        "^Value": [
            {
                "^EncodedType": "org.uberfire.component.model.Project",
                "^ObjectID": "3",
                "name": "p1",
                "selected": true,
                "parent": null,
                "children": {
                    "^EncodedType": "java.util.ArrayList",
                    "^ObjectID": "4",
                    "^Value": [
                        {
                            "^EncodedType": "org.uberfire.component.model.Folder",
                            "^ObjectID": "5",
                            "name": "f1",
                            "parent": {
                                "^EncodedType": "org.uberfire.component.model.Project",
                                "^ObjectID": "3"
                            },
                            "children": {
                                "^EncodedType": "java.util.ArrayList",
                                "^ObjectID": "6",
                                "^Value": [
                                    {
                                        "^EncodedType": "org.uberfire.component.model.Task",
                                        "^ObjectID": "7",
                                        "name": "t1",
                                        "done": false,
                                        "parent": {
                                            "^EncodedType": "org.uberfire.component.model.Folder",
                                            "^ObjectID": "5"
                                        },
                                        "children": {
                                            "^EncodedType": "java.util.ArrayList",
                                            "^ObjectID": "8",
                                            "^Value": []
                                        }
                                    }
                                ]
                            }
                        }
                    ]
                }
            }
        ]
    }
}

Notice that Errai inserts a bunch of metadata into the JSON. This is required so that the marshaller knows exactly how to reconstruct the model when parsing this JSON.

Are we there yet? Are we there yet? Are we...

Don't make me turn this blog around and go back home! Of course we aren't there yet! So, what's next? In the next installment I plan to explore the differences between WorkbenchPartViews and WorkbenchEditors. One could argue that everything a WorkbenchEditor does can also be done by a WorkbenchPartView, but who am I to blow against the wind. Stay tuned.

No comments:

Post a Comment