Thursday, May 19, 2016

Uberfire - The Unanswered Questions: Part 1

Uber...what now? 

Yes, I know it sounds like an unfortunate incident on a California freeway, but Uberfire is actually a Rich Client Platform for building web applications. Based on the Google Web Toolkit (GWT) and JBoss Errai, Uberfire allows you to build workbench-style applications entirely (or rather, mostly) in Java. Think of HTML meets Java with a little Javascript thrown in for good measure. Uberfire is currently used for the KIE Workbench, which has been a driving force in Uberfire's development. BTW, this actually happened to me in college - engine fires were simply accepted as part of the VW Beetle owner experience.

Disclaimer:

Uberfire has a steep learning curve and I'm still climbing that hill. The information in these blogs is my understanding of the software and may or may not be correct. I'm asking my readers to help me out and correct errors or misunderstandings here - thank you :) So with that said, please take heed of my disclaimer*.

Building on UF Tasks

If you haven't realized it by now, Uberfire is huge as far as application frameworks go, and it tries very hard to hide many of the complexities of web app development.

While the original UFTasks tutorial does a good job of presenting some of the major UI framework concepts, there are many aspects of Uberfire which, in my mind at least, were still a mystery and left me wanting more. Some of the unanswered questions that I was thinking about while working through the tutorial were:
  • How does the VFS work, and what are my options for using the client and server side file system? I would have liked to be able to persist the Project/Folder/Task hierarchy that was developed by the UFTasks tutorial. I assume that since the tutorial web app requires a user login, how about a personal task list for each user? How do I get the currently logged-in user's information?
  • What is this CDI thing you speak of? Even though I've been a java developer for years (I come from a C++ background) I have never really dug very deeply into CDI nor understood all of its complexities. Uberfire depends very heavily on using CDI for intra-app communication, so I was going to have to study up on it.
  • Views, shmiews...what I really want is an Editor. Editors are a big part of the Uberfire user experience, but what exactly is a @WorkbenchEditor and how is it different from a @WorkbenchPartView?
I'm only scratching the surface of my "unanswered questions" in part 1 of this blog. As I become more familiar with Uberfire, I'm sure I'll find more unanswered questions along the way.

Rethinking the Model

Thinking about what a task or to-do list really is, the UFTasks demo left me wanting more than just a task name. What about priorities? Start and completion dates? Maybe a more detailed description? I can probably come up with several other things I would want in my task manager, but let's not lose sight of the goal, which is to learn about Uberfire, and not to design the World's Greatest Freaking To-Do  App.

The first thing that struck me about the UFTasks tutorial was that the Model part of the MVP pattern was scattered through some of the Presenter and View bits. This was a little infuriating because of, well... MVP! The Model should be contained in Model classes, not View or controller classes.

Also, if I was going to persist the model, I would need the ability to quickly and easily navigate it and gather up all the related bits for serialization. So without further ado, here are my model classes:

TreeNode.java

package org.uberfire.shared.model;

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

public abstract class TreeNode< PARENT extends TreeNode, CHILD extends TreeNode > {
    private PARENT parent;
    private List< CHILD > children;

    public TreeNode() {
        parent = null;
    }

    abstract public String getName();

    public PARENT getParent() {
        return (PARENT) parent;
    }

    protected void setParent(PARENT parent) {
        this.parent = parent;
    }

    public List< CHILD > getChildren() {
        if (children == null)
            children = new ArrayList< CHILD >();
        return children;
    }

    public void addChild(CHILD child) {
        getChildren().add(child);
        child.setParent(this);
    }

    public void removeChild(TreeNode child) {
        getChildren().remove(child);
        child.setParent(null);
    }
}

Task.java

package org.uberfire.shared.model;

public class Task extends TreeNode< Folder, TreeNode > {
    private String name;
    private boolean done;

    public Task(String name) {
        this.name = name;
        this.done = false;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public boolean isDone() {
        return done;
    }

    public void setDone(boolean done) {
        this.done = done;
    }
}

Folder.java

package org.uberfire.shared.model;

public class Folder extends TreeNode< Folder, Task > {

    private final String name;

    public Folder(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Project.java

package org.uberfire.shared.model;

public class Project extends TreeNode< Project, Folder > {

    private final String name;
    private boolean selected;

    public Project(String name) {
        // this is the root of the tree so it has no parent
        this.name = name;
        this.selected = false;
    }

    public String getName() {
        return name;
    }

    public boolean isSelected() {
        return selected;
    }

    public void setSelected(boolean selected) {
        this.selected = selected;
    }
    
    public int countDoneTasks() {
        int doneTasks = 0;
        for (Folder folder : getChildren()) {
            for (Task task : folder.getChildren()) {
                if (task.isDone()) {
                    ++doneTasks;
                }
            }
        }
        return doneTasks;
    }
    
    public int countTotalTasks() {
        int totalTasks = 0;
        for (Folder folder : getChildren()) {
            totalTasks += folder.getChildren().size();
        }
        return totalTasks;
    }
}

TasksRoot.java

package org.uberfire.shared.model;

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

public class TasksRoot {
    private List< Project > projects = new ArrayList< Project >();
    
    public List< Project > getProjects() {
        return projects;
    }
}

No surprises here. The only things worth mentioning are:

  • TreeNode - a generic tree node class which takes PARENT and CHILD type parameters. Note that Project does not have a parent and Task does not have any children and we could have written TreeRootNode, TreeInternalNode and TreeLeafNode classes, but...meh.
  • TasksRoot - a root object to contain the list of Projects. This will become important during serialization. The instance of this thing is also made available to other beans by way of the @Produces CDI annotation. More about this in Part 2.
  • Project - this includes two convenience functions for counting the total number of tasks and the number of completed tasks. We'll use this in the Dashboard later on.
Obviously these are some drastic changes here, not to mention the addition of an actual Task model object which was conspicuously missing from the original tutorial. In Part 2 of this blog, I'll describe the changes in the Presenter and View components. Surprisingly, these model changes actually simplified the handling in the "P" and "V" bits so we can concentrate on Uberfire functionality instead of littering the code with "M" bits. Stay tuned...


This product is meant for educational purposes only. Any resemblance to real persons, living or dead is purely coincidental. Void where prohibited. Some assembly required. List each check separately by bank number. Batteries not included. Contents may settle during shipment. Use only as directed. No other warranty expressed or implied. Do not use while operating a motor vehicle or heavy equipment. Postage will be paid by addressee. Subject to CAB approval. This is not an offer to sell securities. Apply only to affected area. May be too intense for some viewers. Do not stamp. Use other side for additional listings. For recreational use only. Do not disturb. All models over 18 years of age. If condition persists, consult your physician. No user-serviceable parts inside. Freshest if eaten before date on carton. Subject to change without notice. Times approximate. Simulated picture. No postage necessary if mailed in the United States. Please remain seated until the ride has come to a complete stop. Breaking seal constitutes acceptance of agreement. For off-road use only. As seen on TV. One size fits all. Many suitcases look alike. Contains a substantial amount of non-tobacco ingredients. Colors may, in time, fade. We have sent the forms which seem right for you. Slippery when wet. For office use only. Not affiliated with the American Red Cross. Drop in any mailbox. Edited for television. Keep cool; process promptly. Post office will not deliver without postage. List was current at time of printing. Return to sender, no forwarding order on file, unable to forward. Not responsible for direct, indirect, incidental or consequential damages resulting from any defect, error or failure to perform. At participating locations only. Not the Beatles. Penalty for private use. See label for sequence. Substantial penalty for early withdrawal. Do not write below this line. Falling rock. Lost ticket pays maximum rate. Nap was here. Your canceled check is your receipt. Add toner. Place stamp here. Avoid contact with skin. Sanitized for your protection. Be sure each item is properly endorsed. Sign here without admitting guilt. Slightly higher west of the Mississippi. Employees and their families are not eligible. Beware of dog. Contestants have been briefed on some questions before the show. Limited time offer, call now to ensure prompt delivery. You must be present to win. No passes accepted for this engagement. No purchase necessary. Processed at location stamped in code at top of carton. Shading within a garment may occur. Use only in a well-ventilated area. Keep away from fire or flames. Replace with same type. Approved for veterans. Booths for two or more. Check here if tax deductible. Some equipment shown is optional. Price does not include taxes. No Canadian coins. Not recommended for children. Prerecorded for this time zone. Reproduction strictly prohibited. No solicitors. No alcohol, dogs or horses. No anchovies unless otherwise specified. Restaurant package, not for resale. List at least two alternate dates. First pull up, then pull down. Call toll free number before digging. Driver does not carry cash. Some of the trademarks mentioned in this product appear for identification purposes only. Objects in mirror may be closer than they appear. Record additional transactions on back of previous stub. Unix is a registered trademark of AT&T. Do not fold, spindle or mutilate. No transfers issued until the bus comes to a complete stop. Package sold by weight, not volume. Your mileage may vary. If the flow controls supplied are not installed, this unit will not operate properly. Keep out of reach of children. When this set is used with other equipment, if the picture is not stable or the buzz sound is heard, try to change the mutual position of relevant equipment or take enough distance between them. This unit not labeled for retail sale. Phenylketonurics: contains phenylalanine. Close cover before striking. Mind the gap. No merchantability expressed or implied. Parental discretion is advised. Sold as a novelty item only. Although robust enough for general use, adventures into the esoteric periphery may reveal unexpected quirks. Not available in stores. May cause abdominal cramping and loose stools. Vitamins A, D, E, and K have been added. Not designed or intended for use in on-line control of aircraft, air traffic, aircraft navigation or aircraft communications; or in the design, construction, operation or maintenance of any nuclear facility. Container may explode if heated. May contain traces of various nuts and seeds.

Wednesday, May 18, 2016

Uberfire - The Unanswered Questions: Part 2

And now presenting...

As I mentioned in Part 1, my proposed model changes will necessarily cause some changes in the Presenter and View classes. Most of these are as a consequence of removing the Model bits from Presenter and View.

I have made the design decision to make ProjectsPresenter the "owner" of the model root, listening for model change events from the TasksPresenter and making the model changes. This will also be a good place to handle model persistence. More about this later on, but first let's take a look at the new Presenter and View classes. I don't want to bore you by filling this blog with pages of java code, so I'll just present some of the highlights. The complete tutorial can be found on github here.

ProjectsPresenter.java

The big changes here are the additions of model change handlers: the @Observes annotations identify the change events which are injected into this class by TaskPresenter. This concept was introduced in the original tutorial, and should already be familiar.

The model root object, a TasksRoot instance, is made available with the @Produces annotation. This is consumed by the DashboardPresenter class with @Inject. Consequently, the DashboardPresenter code is greatly simplified because it doesn't have to deal with handling events to update its "total tasks" and "tasks done" counters - it can simply ask the model for this information.

    private TasksRoot tasksRoot = new TasksRoot();

    @Produces
    @Named("tasksRoot")
    public TasksRoot tasksRoot() {
        return tasksRoot;
    }

The other obvious change is that Project, Folder and Task name strings (which are actually attributes of these model objects) have been replaced by their actual model objects, and the object tree hierarchy is strictly modeled.

TasksPresenter.java

Again, the main changes here are replacement of name strings with actual model objects; similar changes were made in the TaskCreated and TaskDone events. These are identical to those in the original tutorial, except for (again) the use of actual model objects instead of strings. I've also added FolderCreated and FolderRemoved events so we can notify the ProjectsPresenter when these are triggered. Recall that the "Create Folder" button is on the TaskView.
    @Inject
    private Event< TaskCreated > taskCreatedEvent;

    @Inject
    private Event< TaskDone > taskDoneEvent;
    
    @Inject
    private Event< FolderCreated > folderCreatedEvent;

    @Inject
    private Event< FolderRemoved > folderRemovedEvent;

    public void newFolder(String folderName) {
        folderCreatedEvent.fire(new FolderCreated(new Folder(folderName)));
        updateView();
    }

    public void removeFolder(Folder folder) {
        folderRemovedEvent.fire(new FolderRemoved(folder));
        updateView();
    }
    
    public void doneTask(Task task) {
        taskDoneEvent.fire(new TaskDone(task.getParent(), task));
        updateView();
    }

    public void createTask(Folder folder, Task task) {
        taskCreatedEvent.fire(new TaskCreated(folder, task));
        updateView();
    }
}

Another thing worth mentioning is that the ProjectsPresenter class notifies TasksPresenter when a different project has been selected. This allows TasksPresenter to redraw its view with content of the new Project.

    private Project currentSelectedProject;

    public void projectSelected(@Observes ProjectSelectedEvent projectSelectedEvent) {
        currentSelectedProject = projectSelectedEvent.getProject();
        selectFolder();
    }

DashboardPresenter.java

As mentioned previously, the code for this class has been reduced to just a few lines because it no longer listens for task events to update its totals. Instead we use an @Inject to get the instance of the model root object (TasksRoot) and ask it for these totals. The Dashboard view remains the same as before.
    @Inject
    @Named("tasksRoot")
    private TasksRoot tasksRoot;

    private void updateView() {
        view.clear();
        for (Project project : tasksRoot.getProjects()) {
            int done = project.countDoneTasks();
            int notDone = project.countTotalTasks() - done;
            view.addProject(project, notDone+"", done+"");
        }
    }



Event Classes

These should be self-explanatory. The TasksPresenter class creates one of these event objects and then fires the event off to any beans that are @Observe'ing these events. Note that the FolderRemoved event is not used yet, but we may want to implement that later.

What's next?

So far I haven't introduced any new Uberfire concepts, just rearranged some bits to make persistence of the model possible. I'll go into this in Part 3 of this blog.

Tuesday, May 17, 2016

Uberfire - The Unanswered Questions: Part 3

The Uberfire VFS

Uberfire supports two types of Virtual File System (VFS) strategies: simple file access using the server host's native file system and a git-based repository file system. As of the current version (0.9.0) of Uberfire, I have only been able to successfully use the git VFS - the simple VFS somehow eludes me. There is also support for client-side file access, and I'll blog about that later.

Before we can talk about the server-side VFS, we need to take a little detour. A typical Uberfire project is split between server-side and client-side java code. The client code is translated from Java to Javascript by the GWT compiler. There are lots of resources on the web that describe how all of this works, so I won't go into a lot of detail here. Suffice it to say that the *.gwt.xml files in your Uberfire project define what is client-side GWT stuff; everything else is server-side stuff. For example, have a look at the UFTasksShowcase.gwt.xml in the uftasks-webapp project:


<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.4.0//EN"
    "http://google-web-toolkit.googlecode.com/svn/tags/2.5.0/distro-source/core/src/gwt-module.dtd">
<module>
  <inherits name="org.uberfire.UberfireAPI"/>
  <inherits name="org.uberfire.UberfireClientAPI"/>
  <inherits name="org.uberfire.UberfireWorkbench"/>
  <inherits name="org.uberfire.UberfireJS"/>
  <inherits name="org.uberfire.UberfireBackend"/>
  <inherits name="org.uberfire.UberfireWorkbenchBackend"/>

  <inherits name="org.uberfire.client.views.PatternFly"/>

  <inherits name="org.uberfire.component.UFTasksComponentClient"/>
  <!-- Specify the paths for translatable code -->
  <source path='client'/>
  <source path='shared'/>

</module>

This file lives in the "org.uberfire" package in your src/main/resources project folder and defines a GWT "module". The <source path='client'> and <source path='shared'> elements declare that all the classes in the org.uberfire.client and org.uberfire.shared packages and their sub-packages are GWT-managed. Everything else (e.g. the stuff in the org.uberfire.backend package) is server-side.

The <inherits> elements define the dependencies required by this module. Java source code for these dependencies must be available to the GWT compiler, and must have their own *.gwt.xml resources, i.e. they must also be GWT modules. For example, if you're using the eclipse IDE to build your Uberfire project, you can see these in the "Maven Dependencies" library:


The Producers

As you have probably already figured out, objects that are @Inject'ed into code must be @Produced from somewhere. In this case, the VFS I/O service instance is produced by a server-side class, named ApplicationScopedProducer in our uftasks-webapp project. The class looks like this:

ApplicationScopedProducer.java

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
import javax.inject.Named;

import org.uberfire.commons.services.cdi.Startup;
import org.uberfire.commons.services.cdi.StartupType;
import org.uberfire.io.IOService;
import org.uberfire.io.impl.IOServiceNio2WrapperImpl;

@Startup(StartupType.BOOTSTRAP)
@ApplicationScoped
public class ApplicationScopedProducer {

    @Inject
    private IOWatchServiceAllImpl watchService;

    private IOService ioService;

    @PostConstruct
    public void setup() {
        ioService  = new IOServiceNio2WrapperImpl("1", watchService);
    }

    @Produces
    @Named("ioStrategy")
    public IOService ioService() {
        return ioService;
    }

}

This class instantiates a default VFS service implementation (IOServiceNio2WrapperImpl) which in this case is a git-based repository, and makes the service available to other application beans. Note the optional use of the @Inject'ed IOWatchServiceAllImpl object. Your application beans can listen for VFS resource changes by @Observe'ing the events broadcast from this service - pretty cool.

Also notice the @Startup(StartupType.BOOTSTRAP) annotation. This guarantees that this bean will be processed during the container's startup sequence and before any other beans. In other words, the ioService will be made available very early on during the app server's lifecycle. Note that this does not "create" a file system interface yet; this is where the next class comes in.

AppSetup.java

package org.uberfire.backend.server;

import java.net.URI;
import java.util.HashMap;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.inject.Named;

import org.uberfire.commons.services.cdi.Startup;
import org.uberfire.io.IOService;
import org.uberfire.java.nio.file.FileSystemAlreadyExistsException;

@ApplicationScoped
@Startup
public class AppSetup {

    @Inject
    @Named("ioStrategy")
    private IOService ioService;

    @PostConstruct
    public void init() {
        try {
            ioService.newFileSystem( URI.create("default://uftasks"),  new HashMap<String, Object>() {{}} );
            
        } catch ( final FileSystemAlreadyExistsException ignore ) {

        }
    }

}

This class uses the ioService object to create the default file system. This bean is also processed during server startup, but only after all of the other BOOTSTRAP beans have been processed. This file system is registered with the Uberfire VFSService by some voodoo magick, and that's how the client code interacts with the server-side file system. Confused yet? Don't worry, it gets even more confusing. Here's a very simple example of client-side code that reads from a VFS file on the server:

    @Inject
    protected Caller<VFSService> vfsServices;

    private void readFile(String fileURI) {
        vfsServices.call(new RemoteCallback<Path>() {
            @Override
            public void callback(final Path path) {
                vfsServices.call(new RemoteCallback<String>() {
                    @Override
                    public void callback(final String response) {
                        GWT.log("Read Response: " + response);
                    }
                }).readAllString(path);
            }
        }).get(fileURI);
    }

Here, the outer vfsServices.call() returns a Path object by way of the callback() method; the inner call() does the actual file reading from the Path object and its callback() returns the file contents as a String.

Notice that, because this is an interaction between the client and server, everything is done asynchronously; that is, the vfsServices.call() returns immediately and then, at some point in the future, the server returns its response by way of the callback() method. It's up to your client code to gracefully handle timeouts while waiting for the server to respond.

There is another way of constructing the Path object without having to go through the vfsServices.call(), and that is to use the PathFactory:

    private final static String DEFAULT_URI = "default://uftasks";
    String filename = "tasks.json";
    String uri = DEFAULT_URI + "/" + "/" + filename;
    Path path = PathFactory.newPath(filename, uri);

and then the file read simply becomes:

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

File writing is similar in structure:
    @Inject
    protected Caller<VFSService> vfsServices;

    private void writeFile(String fileURI, final String content) {
        String filename = "tasks.json";
        String uri = DEFAULT_URI + "/" + "/" + 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);
    }

Putting it all together

Now we have pretty much everything we need to be able to serialize our Tasks model. I'll close the loop on this in my next installment, Part 4.

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.

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.

Saturday, May 14, 2016

Uberfire - The Unanswered Questions: Part 6

TO-DO: Make a TO-DO List

In this session I'll be creating an editor for the TO-DO Task object. I'll start off by duplicating the Presenter-View pattern already established by the other two popups for NewFolder and NewProject. The reason for this is because I want to explore some new GWT widgets for my Task Editor and I don't want to complicate things by adding all of the other junk required to support a WorkbenchEditor. In this round we'll just create a modal popup dialog to host the Task Editor but eventually we may want to place it somewhere in the workbench window.

This first step is dead simple: copy and rename the three files named NewFolder<something> in the org.uberfire.client.screens.popup package and replace "NewFolder" with "TaskEditor". So, you should have TaskEditorPresenter,java TaskEditorView.java and TaskEditorView.html and they should look something like this:

TaskEditorPresenter.java

@Dependent
public class TaskEditorPresenter {

    public interface View extends UberView<TaskEditorPresenter> {

        void show();

        void hide();
    }

    @Inject
    private View view;

    private TasksPresenter tasksPresenter;

    @PostConstruct
    public void setup() {
        view.init( this );
    }

    public void show( TasksPresenter tasksPresenter ) {
        this.tasksPresenter = tasksPresenter;
        view.show();
    }

    public void close() {
        view.hide();
    }
}

TaskEditorView.java

@Dependent
@Templated
public class TaskEditorView extends Composite
        implements TaskEditorPresenter.View {
    
    private TaskEditorPresenter presenter;

    private Modal modal;
    
    @Inject
    @DataField("ok-button")
    Button okButton;

    @Inject
    @DataField("cancel-button")
    Button cancelButton;

    @Override
    public void init( TaskEditorPresenter presenter ) {
        this.presenter = presenter;

        this.modal = new Modal();
        final ModalBody body = new ModalBody();
        body.add( this );
        modal.add( body );
    }

    @Override
    public void show() {
        modal.show();
    }

    @Override
    public void hide() {
        modal.hide();
    }

    @EventHandler("ok-button")
    public void onOk( ClickEvent event ) {
        presenter.close();
    }

    @EventHandler("cancel-button")
    public void onCancel( ClickEvent event ) {
        presenter.close();
    }
}

TaskEditorView.html

<div>
     <form data-field="task-editor-modal">
        <fieldset>
            <legend>Task Editor</legend>
            <div class="form-group">
            </div>
        </fieldset>
    </form>
    <div class="modal-footer">
        <button type="button" class="btn btn-danger" data-field="cancel-button">Cancel</button>
        <button type="button" class="btn btn-primary" data-field="ok-button">OK</button>
    </div>
</div>

Then in TasksPresenter.java add this:

import org.uberfire.client.screens.popup.TaskEditorPresenter;

    @Inject
    private TaskEditorPresenter taskEditorPresenter;

    public void showTaskEditor(Task task) {
        taskEditorPresenter.show(this);
    }

and in TasksView.java change the generateTasks() method to this:

    private ListGroupItem generateTask(Task task) {
        TaskItem taskItem = new TaskItem(task);
        taskItem.add(createTaskCheckbox(task));
        taskItem.add(createTaskNotesButton(task));

        return taskItem;
    }

This will append a button with an EDIT icon to the name in the task list. Finally add the createTasksNotesButton method:

import org.gwtbootstrap3.client.ui.constants.IconType;

    private Button createTaskNotesButton(Task task) {
        Button button = GWT.create(Button.class);
        button.setIcon(IconType.EDIT);
        button.setMarginLeft(20.0);
        button.addClickHandler(event -> presenter.showTaskEditor(task));
        return button;
    }

Rebuild the whole mess and run it - you should that the tasks in the list now have an EDIT icon next to them.


When you click that icon you'll just get an empty modal popup dialog with an "OK" and "Cancel" button. And now, it's time for...

Fun With Widgets!

At the beginning of this series I mentioned that a Task should have more than just a brief description (the "name" attribute) mostly because I needed an excuse to build on an already freaking awesome web app. It would be nice to be able to add some notes, possibly in the form of a Rich Text document with 4x8 color glossy pictures and circles and arrows and such. I also need to be able to prioritize my TO-DO list, so let's assign a numeric priority value to each Task. Finally, I would like to know when a Task needs to be completed, so each Task needs a due date.

If all this sounds a bit contrived, that's because it is - it's an excuse to have some Fun With Widgets!

Rich Text

The GWT Rich Text Area is a text editor that allows complex styling and formatting. This widget is browser dependent and may not work as expected for all browsers.

We'll use this for our Task notes field, so let's go ahead and add one of these to our popup. First, add an anchor to TaskEditorView.html:

     <form data-field="task-editor-modal">
        <fieldset>
            <legend>Task Editor</legend>
            <div class="form-group">
                <a data-field="task-notes"></a>
            </div>
        </fieldset>
    </form>

Then create the Rich Text Area in TaskEditorView.java and attach it to the anchor:

    RichTextArea notesTextArea;
    
    @Inject
    @DataField("task-notes")
    Anchor taskNotesAnchor;

    @Override
    public void init( TaskEditorPresenter presenter ) {
        ...

        // add the other widgets
        notesTextArea = new RichTextArea(); 
        notesTextArea.setHeight("200");
        notesTextArea.setWidth("100%");
        notesTextArea.setHTML("<b>Hello World!</b><br/>Uberfire is cool");
        taskNotesAnchor.add(notesTextArea);
    }

List Box (a.k.a. "ComboBox")

This is general-purpose selection list which can either be the drop-down "Combo Box" style, or simply a list of selections. We're going to use this for our Task priority field. Again, in the html template, add an anchor for our ListBox widget:


     <form data-field="task-editor-modal">
        <fieldset>
            <legend>Task Editor</legend>
            <div class="form-group">
                <a data-field="task-notes"></a>
                <b>Priority:</b> <a data-field="task-priority"></a>
            </div>
        </fieldset>
    </form>

And in the View class:

    @Inject
    @DataField("task-priority")
    Anchor taskPriorityAnchor;

    ListBox priorityListBox;

    @Override
    public void init( TaskEditorPresenter presenter ) {
        ...

        priorityListBox = new ListBox();
        priorityListBox.setMultipleSelect(false);
        priorityListBox.addItem("Medium", "2");
        priorityListBox.addItem("High", "3");
        priorityListBox.addItem("Low", "1");
        priorityListBox.addItem("Screw it, I don't time for this crap!", "0");
        taskPriorityAnchor.add(priorityListBox);
    }

Date Picker

Finally, let's add a Date Picker widget for the Task due date. In the html template add an anchor for the widget:

     <form data-field="task-editor-modal">
        <fieldset>
            <legend>Task Editor</legend>
            <div class="form-group">
                <a data-field="task-notes"></a>
                <b>Due Date:</b> <label data-field="task-due-date-label">Due Date</label> <a data-field="task-due-date"></a>
                <b>Priority:</b> <a data-field="task-priority"></a>
            </div>
        </fieldset>
    </form>

And add the relevant bits in the View class:

    public static class DueDateValueChangeHandler implements ValueChangeHandler<Date> {
        private final Label text;

        public DueDateValueChangeHandler(Label text) {
            this.text = text;
        }

        public void onValueChange(ValueChangeEvent<Date> event) {
            Date date = event.getValue();
            String dateString = DateTimeFormat.getFormat(PredefinedFormat.DATE_MEDIUM).format(date);
            text.setText(dateString);
        }
    }
   
    @Inject
    @DataField("task-due-date")
    Anchor taskDueDateAnchor;

    @Inject
    @DataField("task-due-date-label")
    Label dueDateLabel;

    DatePicker dueDatePicker;

    @Override
    public void init( TaskEditorPresenter presenter ) {
        ...

        dueDatePicker = new DatePicker();
        dueDatePicker.setYearArrowsVisible(true);
        dueDatePicker.setYearAndMonthDropdownVisible(true);
        dueDatePicker.setVisibleYearCount(51);
        dueDatePicker.addValueChangeHandler(new DueDateValueChangeHandler(dueDateLabel));
        dueDatePicker.setValue(new Date(), true);
        taskDueDateAnchor.add(dueDatePicker);
    }

Note the internal class DueDateValueChangeHandler. This is called when the user selects a date from the DatePicker and updates  the Due Date label in the html template.

Stylings

The default DatePicker presentation is pretty lame so I've added a stylesheet to make it look a bit better. A note about CSS or stylesheets: If you create a ".css" file with the same name as your html template file, Errai will automatically load that file and apply the styles to the widgets in your template. In this case, I have defined some styles for the gwt-DatePicker in a file named TaskEditorView.css.

You can also specify a stylesheet using @Templated(stylesheet=“some/other/path/stylesheet.css”) in your Viewer java class.

Also, the usual stylesheet inclusion methods still work in Errai: any styles or CSS files included in your src/main/webapp/index.html or src/main/webapp/login.jsp will be available to the entire application.

I'll save you the boring details of TaskEditorView.css here; instead grab the latest tag from my repository and enjoy.

Let 'er Rip!

Let's build and run this sucker and see what happens. If everything went right, you should see this popup when you click on one of the Task EDIT buttons:


What About WorkbenchEditor?

I know that last time I promised to dive deeper into the inner workings of a WorkbenchEditor, but I think it was important to explore the relationship between the html template and view and have some Fun With Widgets! I promise to show you how WorkbenchEditor works in Part 7 of this series.

Friday, May 13, 2016

Uberfire - The Unanswered Questions: Part 7

Good News Everyone!

...is what I would have said if there had been good news. The news is that, as of June 8, Uberfire has migrated from wildfly 8.1 to wildfly 10 and GWT's SuperDevMode will no longer work with 8.1. Well, it actually is good news because, considering version 10 has been around for more than a year, Uberfire has finally caught up. The "not so good news, everyone" is that we'll have to make some minor changes to our maven pom files to use the new wildfly version so we can use SuperDevMode to debug our web app.


This Week's Blog

In this installment I will finally introduce Uberfire WorkbenchEditors, and explain how they differ from Screens. This will require some additional GWT and Uberfire components that were not included in previous versions of our UFTasks tutorial, which means changes to some of the build files.

I'll also introduce some new concepts that are unique to editors, and I'll point out some restrictions imposed by the Uberfire framework for launching and passing information to/from editors. Let's get started...


pom /ˈpäm/
noun
one half of a pom-pom





First let's address the wildfly 10 change: in the pom.xml file located in the uftasks-webapp directory, look for the <as.version> property at the top of the file and change its value to 10.0.0.Final. So, you should have this:

    <as.version>10.0.0.Final</as.version>

As long as we're mucking around in here, let's also add the necessary dependencies to support our WorkbenchEditor; add the following dependencies to the pom.xml:

    <dependency>
      <groupId>org.uberfire</groupId>
      <artifactId>uberfire-workbench-processors</artifactId>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>org.uberfire</groupId>
      <artifactId>uberfire-commons-editor-client</artifactId>
    </dependency>

    <dependency>
      <groupId>org.uberfire</groupId>
      <artifactId>uberfire-commons-editor-api</artifactId>
    </dependency>

    <dependency>
      <groupId>org.uberfire</groupId>
      <artifactId>uberfire-widgets-commons</artifactId>
    </dependency>

    <dependency>
      <groupId>org.uberfire</groupId>
      <artifactId>uberfire-widgets-core-client</artifactId>
    </dependency>

and in the <compileSourcesArtifacts> section, add these lines:

  <compileSourcesArtifact>org.uberfire:uberfire-commons-editor-api</compileSourcesArtifact>
  <compileSourcesArtifact>org.uberfire:uberfire-commons-editor-client</compileSourcesArtifact>
  <compileSourcesArtifact>org.uberfire:uberfire-widgets-core-client</compileSourcesArtifact>
  <compileSourcesArtifact>org.uberfire:uberfire-widgets-commons</compileSourcesArtifact>
  <compileSourcesArtifact>org.uberfire:uberfire-widgets-table</compileSourcesArtifact>
  <compileSourcesArtifact>org.uberfire:uberfire-widgets-properties-editor-api</compileSourcesArtifact>
  <compileSourcesArtifact>org.uberfire:uberfire-widgets-properties-editor-client</compileSourcesArtifact>
  <compileSourcesArtifact>org.uberfire:uberfire-widgets-service-api</compileSourcesArtifact>

That should be all that is needed; rebuild the entire application and test to make sure that it still works, and that it now downloads and installs wildfly 10.

Place Manager and Place Requests

The original UFTasks tutorial briefly mentioned the Place Manager and the concept of "places". We saw this service used in the ShowcaseEntryPoint class of our application to open different Perspectives. The Place Manager is used in general to open any kind of "managed window", and tracks which are currently active. The term "managed window" refers to any one of a number of different types of windows decorated with one of the annotations defined in the org.uberfire.client.annotations package; some of these you already know, e.g. WorkbenchScreen, WorkbenchEditor and Perspective. There are other types of managed windows, which I'll discuss in future blogs, but for now let's concentrate on the ones we know about.

The Place Manager keeps an internal lookup table of all currently open managed windows. The lookup key varies, depending on the type of window.

When your application wants to open a Screen, Perspective, or an Editor, it needs to construct a PlaceRequest. For Screens (and Perspectives and certain other types of managed windows), the PlaceRequest is simple: it only needs the Screen identifier, declared in the @WorkbenchScreen annotation as in, for example:

@WorkbenchScreen(identifier = "ProjectsPresenter")

This is because your application can only have one instance of a Screen type open at a time. The PlaceRequest is then simply:

PlaceRequest pr = new DefaultPlaceRequest("ProjectsPresenter");

Workbench Editors, however can have multiple instances of the same type open at the same time, so the Place Request uses the file Path being edited as the lookup table key. In this case we need to use a PathPlaceRequest, like so:

Path path = getFilePathToEdit();
PlaceRequest pr = new PathPlaceRequest(path);

We'll see how this works later.

The Workbench Editor

A WorkbenchEditor is constructed the same as any of our other Presenter classes, except it uses the @WorkbenchEditor annotation:

@WorkbenchEditor(identifier = "TaskEditor", supportedTypes = { TaskResourceType.class })

The "supportedTypes" attribute tells the Place Manager the file types (actually the filename extensions) to associate with this editor.

An Editor also needs to provide some additional UI and lifecycle bits, which are identified by annotations. These are:

  • @PostConstruct - method that is called immediately after construction of the editor Presenter class to initialize the editor's View class
  • @OnStartup - startup method that is called after the @PostConstruct method, which is responsible for initializing internal data structures and loading the editor content
  • @WorkbenchPartTitle - a method that returns the title text for the editor
  • @WorkbenchPartTitleDecoration - optional method that returns the editor title widget
  • @WorkbenchPartView - method that returns the View for the editor
  • @WorkbenchMenu - method that returns an editor-specific menu, which may be null

I won't go into the excruciatingly boring details of the implementations of each of these methods. As always, the current code can be downloaded from the git repository.

Resource Types

A Resource Type simply identifies the content of a file, similar in concept to the Eclipse Content Types, except that Uberfire associates only the file extension with the Resource Type - there is no "peeking" inside the file to determine what's in it.

When defining a Workbench Editor, you must provide one or more Resource Types that the editor can handle. For example, a text editor may declare that it can be used for both a Text Resource Type and an XML Resource Type. The editor may also declare a "priority" to help Uberfire determine the best editor to use for a particular Resource Type. Thus, if your application contains both an advanced XML editor with syntax highlighting and tag completion and a bunch of other cool stuff, along with a plain text editor, Uberfire will choose the XML editor if it declares a higher priority in the @WorkbenchEditor annotation.

For our Task Editor we will create a new ResourceType to handle Task objects. The ResourceType definition looks like this:

@ApplicationScoped
public class TaskResourceType implements ClientResourceType {

    @Override
    public String getShortName() {
        return "task";
    }

    @Override
    public String getDescription() {
        return "TO-DO Task file";
    }

    @Override
    public String getPrefix() {
        return "";
    }

    @Override
    public String getSuffix() {
        return "task";
    }

    @Override
    public int getPriority() {
        return 0;
    }

    @Override
    public String getSimpleWildcardPattern() {
        return "*.task";
    }

    @Override
    public boolean accept(Path path) {
        return path.getFileName().endsWith( "." + getSuffix() );
    }

    @Override
    public IsWidget getIcon() {
        return null;
    }

}

This is pretty straightforward: it simply defines a file extension of "task" and provides some descriptive text and an icon for use with, for example, a file browser screen if one were provided. Now when we ask the Place Manager to open a VFS file with a ".task" extension, it will know to instantiate our Task Editor.

The View

Surprisingly, the Task editor view has not changed very much from its previous incarnation, which was hosted in a popup modal dialog box. Obviously the show() and hide() methods, which controlled the visibility of the modal dialog, have gone away. The TaskEditorView interface now looks like this (recall that this interface is defined internally in TaskEditorPresenter):

    public interface View extends UberView<TaskEditorPresenter> {
        IsWidget getTitleWidget();
        void setContent(final TaskWithNotes content);
        TaskWithNotes getContent();
        boolean isDirty();
    }

The getTitleWidget() method satisfies the @WorkbenchPartTitleDecoration requirement for the Presenter. The setContent() and getContent() methods deserve some explanation (see Argh! More Model Changes?) and the isDirty() method is used by the Presenter to determine if the Task object has changed and needs to be persisted.

Firing up the Editor

In the previous incantation of this code, we opened the modal dialog popup used to edit the Task, from the TasksPresenter. Recall that the associated View had an "edit" button for each task which, when clicked, caused the dialog to show. We'll still keep this method for opening the Task Editor, but instead we'll use a Place Manager request to open our Workbench Editor. Our showTaskEditor() method in TasksPresenter now looks like this:

    public void showTaskEditor(final Task task) {
        this.task = task;
        ufTasksService.call(new RemoteCallback<String>() {
            @Override
            public void callback(final String response) {
                if (response!=null) {
                    String filename = response.replaceFirst(".*/", "");
                    Path path = PathFactory.newPath(filename, response);
                    placeRequest = new PathPlaceRequest(path);
                    placeManager.goTo(placeRequest);
                }
                else 
                    GWT.log("UFTasksService is unable to load tasks file");
            }
        }).getFilePath(user.getIdentifier(), task);
    }

    public PathPlaceRequest getPlaceRequest() {
        return placeRequest;
    }
    
    public Task getTask() {
        return task;
    }

Here, we have added a new method to the UFTasksService called getFilePath(). This returns a file name string for the task notes file (the "*.task" ResourceType.) Since the UFTasksService already knows the file system location of the Tasks list file ("tasks.json") it seemed like a logical place to provide the task notes file name as well. The task notes file name is simply a concatenation of the User ID, Task ID and the ".task" file name suffix.

Notice that the placeRequest object is a TasksPresenter class field, which is provided to the TaskEditorPresenter by way of getPlaceRequest(). This is required so that the Editor can properly close the window when requested by the user. Apparently Place Manager requires the same PlaceRequest object that opened an editor, to be used to close it as well. This is because it uses the hashcode of the PlaceRequest object as the key in its internal lookup table. It beats the heck outta me why this is so, but...there it is. Maybe one of the Uberfire gurus can explain why it's done this way instead of using the PlaceRequest identifier and/or Path value. This brings me to my next topic:

Parameter Passing

Place Manager only allows String name/value pairs to be passed into an Editor. This can be done either in one of the PlaceRequest constructors (DefaultPlaceRequest(identifier, Map<String,String> parameters)) or using the addParameter() method after construction. This means that if you want to pass an object into your Editor, you'll have to serialize the object as String name/value pairs, or as JSON - bummer :(

Notice that I chose to provide the Task object being edited by way of TasksPresenter#getTask() instead of serializing it and passing it through the Place Request parameters map. If there's a better way of doing this, I'd be interested to find out.

Argh! More Model Changes?

Oh what, you thought we were done hacking around in the model? First, we haven't even defined the additional bits of information that we wanted to include in a Task object (priority, due date, etc.) and second, the Presenter-Model interaction will require some additional support from the Model.

Besides the aforementioned bits, we also want the Task to have an arbitrarily long Rich Text "notes" attribute which the user can view and update inside our Task Editor, but we don't want to incur the penalty of having to marshal this potentially large amount of data across the wire for every task displayed by the TasksPresenter/TasksView list. Instead we only want to load the notes when the user opens the Task Editor. This is accomplished using a separate text file associated with a Task instance. The text file name is constructed using the Task's "id" field suffixed with a ".task" file extension. Thus, when we issue a Place Request with this file name, Place Manager will locate and open our Editor.

Our new Task object now contains the following fields (along with getters and setters):

public class Task extends TreeNode<Folder, TreeNode> {
    private String name;
    private boolean done;
    private int priority;
    private Date dueDate;
    private String id;

    public Task(@MapsTo("name") String name) {
        this.name = name;
        this.done = false;
        priority = 0;
        dueDate = new Date();
        
        // Yes we should probably use a UUID here to ensure uniqueness,
        // but this is good enough for our purposes...
        this.id = Long.toString(System.currentTimeMillis());
    }
    
    public Task(Task that) {
        set(that);
    }

    ...

    public boolean equals(Object obj) {
        if (obj instanceof Task) {
            Task that = (Task)obj;
            if (!this.getName().equals(that.getName()))
                return false;
            if (this.isDone() != that.isDone())
                return false;
            if (this.getPriority() != that.getPriority())
                return false;
            if (!this.getDueDate().equals(that.getDueDate()))
                return false;
            return true;
        }
        return super.equals(obj);
    }

    public void set(Task that) {
        this.name = that.name;
        this.done = that.done;
        this.priority = that.priority;
        this.dueDate = that.dueDate;
        this.id = that.id;
    }

Note the equals() override allows us to determine if a Task has changed (is dirty) in the Editor.

To simplify handling of the additional Rich Text field, I have defined a new Model object named TaskWithNotes. This is for client-side consumption only, so there's no need to provide any kind of marshalling support as we did with the other Model objects. And, here it is:

public class TaskWithNotes extends Task {

    private String notes = "";
     
    public TaskWithNotes(Task that) {
        super(that);
    }

    public void setNotes(String notes) {
        this.notes = notes;
    }
    
    public String getNotes() {
        return notes;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof TaskWithNotes) {
            TaskWithNotes other = (TaskWithNotes)obj;
            if (!this.getNotes().equals(other.getNotes()))
                return false;
        }
        return super.equals(obj);
    }
    
    public Task asTask() {
        return new Task(this);
    }
}

The constructor initializes the Task base class from a given Task object. An equals() override is used by the Presenter to determine if the object has changed (is dirty).

Now, finally back to our TaskEditorView, which is responsible for setting the Task content into its various widgets. Here are the relevant bits of code:

    @Override
    public void setContent(TaskWithNotes content) {
        taskWithNotes = content;
        nameTextBox.setText(taskWithNotes.getName());
        doneCheckBox.setValue(taskWithNotes.isDone());
        notesTextArea.setHTML(taskWithNotes.getNotes());
        for (int index=0; index<priorityListBox.getItemCount(); ++index) {
            int v = Integer.parseInt(priorityListBox.getValue(index));
            if (v == taskWithNotes.getPriority()) {
                priorityListBox.setSelectedIndex(index);
                break;
            }
        }
        dueDatePicker.setValue(taskWithNotes.getDueDate(), true);
        dueDatePicker.setCurrentMonth(taskWithNotes.getDueDate());
    }

    @Override
    public TaskWithNotes getContent() {
        TaskWithNotes content = new TaskWithNotes(taskWithNotes);
        content.setName(nameTextBox.getText());
        content.setDone(doneCheckBox.getValue());
        content.setNotes(notesTextArea.getHTML());
        content.setPriority(Integer.parseInt(priorityListBox.getSelectedValue()));
        content.setDueDate(dueDatePicker.getValue());
        return content;
    }

The setContent() method saves the original TaskWithNotes object, for comparison with the (possibly) changed values, and then initializes the relevant widgets with the Task values.

The getContent() method reads the widget values and builds a TaskWithNotes object. This can be used to determine if anything has changed, and to serialize the changes to our VFS.

Returning Values

As before, the Editor still has the "OK" and "Cancel" buttons which are used to save/close the Editor. The question now is: how do we get the changed Task values back to the other Presenters/Views in our application? I've chosen to use a notification event, TaskChanged, which simply contains the updated Task object. The event is fired from the Task Editor if anything has changed. This is identical to the other event notification patterns we have already seen being used (TaskCreated, TaskDone, etc.)

The ProjectsPresenter and TasksPresenter Screens both listen for this event: ProjectsPresenter updates the Task in TasksRoot and serializes the entire tree to the "tasks.json" file. TasksPresenter simply updates its View to show the changed values.

Good things don't come easy...or do they?

Wow, that was a little more involved than I originally thought, but totally worth it. I'd be interested to hear if there are better ways to accomplish what I've hacked together here. Comments welcome!