Monday, May 16, 2016

Uberfire - The Unanswered Questions: Part 4

Dude, where's my file?

Default VFS Root

In this installment I'll be adding serialization of the Tasks to our project. We will want to be able to look at what's actually being written to the Uberfire VFS, but to do that we need to know where on disk the file system is actually being created.

By default, the VFS root is in a directory called ".niogit" and is created in the directory from which the web server is started, i.e. the "current directory". Assume that we have installed wildfly (or tomcat, or some other server) in C:/JBoss/wildfly. The server startup scripts are in the "bin" directory, so if we start wildfly like so (Note: I'm using the standard git bash shell here):

$ cd C:/JBoss/wildfly
$ bin/standalone.sh

In this case the VFS root will be in C:/JBoss/wildfly/.niogit and our git VFS directory will be C:/JBoss/wildfly/.niogit/uftasks.git. Note that this git directory is in a "detached head" state. To actually see the files in the working tree we can simply clone the repository:

$ cd C:/JBoss/wildfly/.niogit
$ git clone uftasks.git
Cloning into 'uftasks'...
done.

This will clone uftasks.git into the directory uftasks, which can then be treated as a normal git repository. Remember to "git pull" in this repository whenever you want to get the latest updates from the VFS. If you make changes to files in this repository, they can be committed and pushed back upstream to the Uberfire VFS.

Relocating the VFS Root

Sometimes it's useful to change the VFS root so that it points to some other directory, or some other drive. This can be done by setting a system property in the server's configuration. See App Server Configuration for more information about how this is done. Essentially, it involves editing an app server configuration file and setting the server property "org.uberfire.nio.git.dir" to point to VFS root.

During development/testing you can also change the VFS root by adding a Java VM argument to the maven pom. Locate the pom.xml file in the uftasks-webapp project and add the following to the <extraJvmArgs> element:

  <build>
    ...
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>gwt-maven-plugin</artifactId>
        <configuration>
          <extraJvmArgs>-Dorg.uberfire.nio.git.dir=C:/temp -Xmx1536m -XX:CompileThreshold=7000 -Derrai.jboss.home=${errai.jboss.home}</extraJvmArgs>

This will relocate the VFS root to C:/temp/.niogit.

Tasks Serialization

We'll be using JSON to serialize the Projects/Folders/Tasks hierarchy and, since ProjectsPresenter is the owner of the "tasksRoot" object, it makes sense to add the reading/writing code to this class. To do this, we need to import the com.google.gwt.json.client package. Note that we can't use just any old JSON library because this code will live on the client side and will be translated from Java to Javascript and the source code needs to available to the GWT compiler. In general, it's safe to assume that any Google GWT packages will be OK to use on the client side.

ProjectsPresenter.java

JSON parsing/serializing is tedious, but relatively straight-forward. Add the following methods to this presenter class:

    
    @Inject
    private User user;

    private void saveTasksRoot() {
        JSONObject tasksRootJson = new JSONObject();
        JSONArray projectsJson = new JSONArray();
        int pi = 0;
        for (Project p : tasksRoot.getProjects()) {
            JSONObject pj = new JSONObject();
            pj.put("name", new JSONString(p.getName()));
            
            JSONArray foldersJson = new JSONArray();
            int fi = 0;
            for (Folder f : p.getChildren()) {
                JSONObject fj = new JSONObject();
                fj.put("name", new JSONString(f.getName()));
                foldersJson.set(fi++, fj);
                
                JSONArray tasksJson = new JSONArray();
                int ti = 0;
                for (Task t : f.getChildren()) {
                    JSONObject tj = new JSONObject();
                    tj.put("name", new JSONString(t.getName()));
                    tj.put("isDone", JSONBoolean.getInstance(t.isDone()));
                    tasksJson.set(ti++, tj);
                }
                fj.put("tasks", tasksJson);
            }
            pj.put("folders", foldersJson);
            projectsJson.set(pi++, pj);
        }
        tasksRootJson.put("projects", projectsJson);
        
        final String content = tasksRootJson.toString();
        String filename = "tasks.json";
        String uri = DEFAULT_URI + "/" + user.getIdentifier() + "/" + filename;
        Path path = PathFactory.newPath(filename, uri);

        vfsServices.call(new RemoteCallback<Path>() {
            @Override
            public void callback(final Path response) {
                GWT.log("Write Response: " + response);
            }
        }).write(path, content);
    }

    private void loadTasksRoot() {
        String filename = "tasks.json";
        String uri = DEFAULT_URI + "/" + user.getIdentifier() + "/" + filename;
        Path path = PathFactory.newPath(filename, uri);

        vfsServices.call(new RemoteCallback<String>() {
            @Override
            public void callback(final String response) {
                TasksRoot newRoot = new TasksRoot();
                JSONObject tasksRootJson = JSONParser.parseStrict(response).isObject();
                JSONArray projectsJson = tasksRootJson.get("projects").isArray();
                for (int pi=0; pi<projectsJson.size(); ++pi) {
                    JSONObject pj = projectsJson.get(pi).isObject();
                    Project p = new Project(pj.get("name").isString().stringValue());
                    JSONArray foldersJson = pj.get("folders").isArray();
                    for (int fi=0; fi<foldersJson.size(); ++fi) {
                        JSONObject fj = foldersJson.get(fi).isObject();
                        Folder f = new Folder(fj.get("name").isString().stringValue());
                        JSONArray tasksJson = fj.get("tasks").isArray();
                        for (int ti=0; ti<tasksJson.size(); ++ti) {
                            JSONObject tj = tasksJson.get(ti).isObject();
                            Task t = new Task(tj.get("name").isString().stringValue());
                            t.setDone(tj.get("isDone").isBoolean().booleanValue());
                            f.getChildren().add(t);
                        }
                        p.getChildren().add(f);
                    }
                    newRoot.getProjects().add(p);
                }
                tasksRoot = newRoot;
                updateView();
            }
        }).readAllString(path);
    }


Here we have @Inject'ed the User object, which represents the currently logged-in user. We'll use this user ID as the directory name into which the file "tasks.json" will be written. That ensures that each user will have his/her own task list.

Next, we want the loadTasksRoot() method to be called as soon as this presenter is loaded. This is done using the @PostConstruct CDI annotation:
    @PostConstruct
    public void init() {
        loadTasksRoot();
    }
    

Immediately after the JSON file has been read we want to update the Projects view using ProjectsPresenter.updateView(). But, recall that file reading happens asynchronously, which is why the call to updateView() is made within the reader callback() method instead of in our ProjectsPresenter.init() method.
Finally, we will want to serialize the Tasks tree whenever a change happens in the model. This means we need to add a call to saveTasksRoot() in all of the event handlers, e.g.:

    public void taskCreated(@Observes TaskCreated taskCreated) {
        if (activeProject!=null) {
            Folder folder = taskCreated.getFolder();
            Task task = taskCreated.getTask();
            folder.addChild(task);
            saveTasksRoot();
            updateView();
        }
    }

That's pretty much all there's to it. When we run this app, we should be able to see the changes on the file system. Let's assume we've relocated the VFS file system to our C:/temp directory (see above.) The GWT JSON serializer is fairly simplistic and just spits out everything on one line, but if you have python installed you can pretty-print the JSON to make it more readable. Run the following commands in a git shell and you should see something like this:
$ cd C:/temp/.niogit
$ git clone uftasks.git
Cloning into 'uftasks'...
done.

$ cd uftasks
$ git pull
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (4/4), done.
From C:/temp/.niogit/uftasks
   82468cf..0636b2a  master     -> origin/master
Updating 82468cf..0636b2a
Fast-forward
 admin/tasks.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

$ py -m json.tool <admin/tasks.json
{
    "projects": [
        {
            "name": "Project 1",
            "folders": [
                {
                    "name": "Folder 1",
                    "tasks": [
                        {
                            "name": "Task 1",
                            "isDone": false
                        },
                        {
                            "name": "Task 2",
                            "isDone": true
                        }
                    ]
                },
                {
                    "name": "Folder 2",
                    "tasks": [
                        {
                            "name": "Task 3",
                            "isDone": false
                        },
                        {
                            "name": "Task 4",
                            "isDone": false
                        }
                    ]
                }
            ]
        },
        {
            "name": "Project 2",
            "folders": [
                {
                    "name": "Folder 4",
                    "tasks": [
                        {
                            "name": "Task 7",
                            "isDone": true
                        },
                        {
                            "name": "Task 8",
                            "isDone": true
                        }
                    ]
                }
            ]
        }
    ]
}

What's next?

Ideally we don't want to clutter the ProjectsPresenter with a bunch of JSON parsing/serialization code because 1. it doesn't really belong there and 2. the serialization strategy is tightly coupled to the presenter which we want to avoid (what if we decide to use XML instead?)

In the next installment I'll present a way to decouple serialization from UI code which also makes it possible to do serialization from either the client or server side.

4 comments:

  1. bob: With Errai you should be able to serialise the pojo in one line, you don't need to build JSONObject's manually.

    ReplyDelete
  2. https://github.com/romartin/wirez/blob/master/wirez-backend/src/main/java/org/wirez/backend/definition/marshall/DefaultDiagramMarshaller.java#L41

    ReplyDelete
  3. heh, should have read blog 5. Seems you got there in the end :)

    ReplyDelete
    Replies
    1. Yep. As I mentioned, I'll be taking some wrong turns as I go along but I'll get there eventually ;)

      Delete