I thought I was ready to retire when I resigned my job at Red Hat, but I've been sucked back in to the melee that is software development. Watch this space for some fun new projects and tutorials.
Here's a little preview:
Happy Holidays everyone!
Bob's Biz Buzz
All the buzz that's biz to Bob
Friday, December 20, 2019
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.
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.
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:
- a service interface definition
- a service implementation
- 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 Listprojects = 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:
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?
Subscribe to:
Posts (Atom)