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.

4 comments:

  1. Good stuff Bob, glad you're blogging "as you learn"..

    Regarding the two nested async calls for handling file reads and writes. I guess the "recommended" approach would be to get the "Paths" to the client in the first instance; e.g. your Task List's "getTasks()" type service call would return a collection of Tasks and the Task object contains the Path. Then you'd be able to read/write/delete individual tasks in one operation..

    ReplyDelete
  2. Uhhh...I don't get it. How does the Task object contain the Path? Where is it?

    ReplyDelete
  3. Presumably Tasks are persisted in VFS and you have a server-side service to load Tasks. Path would be a property of Task and when instantiating Task objects on the server you'd populate their Path property. We can talk on IRC if it helps.

    ReplyDelete
  4. Presumably Tasks are persisted in VFS and you have a server-side service to load Tasks. Path would be a property of Task and when instantiating Task objects on the server you'd populate their Path property. We can talk on IRC if it helps.

    ReplyDelete