Monday, December 1, 2008

GXT Tree and Loading with REST

Anyone who has used GXT can tell you that it is very powerful, but examples (and the javadoc) are still few and hard to find. The examples that come with the SDK are great (see Ext GWT 1.2 Samples), but if you need to something beyond these you typically need to figure it out on your own. I hope to be able to write a series of blog articles on some of the designs I have figured out for the conversion of my Stamp application from Swing to GXT. (Some of my readers may remember I was experimenting with EXT-JS (more on this in a future blog)).
Getting a tree to work in GXT is not too hard. In fact the demos do a pretty job of illustrating this. However, my tree was a little more complex than the trees shown. First some features of my tree:
  1. Has three separate types of objects (Stamp Collections, Albums and Countries)
  2. Stamp Collections and Albums may contain children (Albums and Countries respectfully)
  3. A Restful WebService is used to return the content of a particular item. (for example to get the albums of a stamp collection with id of "5" the URI might look like http://hostname/web-app/resources/collections/albums/5).

My original approach was that I wanted to make a request on the selected tree node when an expansion occured. However when I used filtering, this caused the invocation of the filter to actually load all the nodes (which was very slow on the first filter). You can turn this off, however that defeated the point of the filter. It also didn't seem like the right solution since at least in the examples the trees were being loaded via an RPC call. So the problem really boiled down to making the tree make an AJAX call to the Restful Web Service upon requesting the expansion of an unloaded node. First I should mention that trees in GXT actually act on BaseModel objects. One point of interest here is that BaseModel object are not TreeModel's (in this way it does work differently than Swing-based trees).
The approach I took was to create a customized DataProxy and pass this proxy to the store on creation. Thus as nodes are loaded/expanded the data proxy will be used to obtain the node's children. My proxy essentially overloaded the load( ) method and for convienence added another method getRequestBuilder( ). I would send requests using the output of the request builder to the Restful Web Service and process the results with a JSONReader and output the load result in the callback's onSuccess( ) method. So lets look at some code:
public class StampTreeDataProxy implements DataProxy<BaseModel,List<BaseModel>> {

   public StampTreeDataProxy( ) {
      super();
   }

   /**  
    * Generate the request builder for the object. If it is the root node (ie. null)
    * then request the stamp collections.  Else, get the appropriate Albums or Countries.
    * For any other type of object, since we do not know how to build a Restful Web
    * URI, simply return a null value.
    * 
    * @param item   The current (parent) item.
    */
   protected RequestBuilder getRequestBuilder( BaseModel item ) {
      RequestBuilder builder = null;
      if( item == null ) {
 builder = HttpUtils.getRestfulRequest(StampModelTypeHelper.getResourceName(StampCollection.class));
      } else if( item instanceof NamedBaseTreeModel ){
         String pathInfo = ((item instanceof Album) ?
                StampModelTypeHelper.getResourceName(Country.class) :    
                StampModelTypeHelper.getResourceName(Album.class)) +"/" + ((NamedBaseTreeModel)item).getId();
         builder = HttpUtils.getRestfulRequest(
                StampModelTypeHelper.getResourceName(item.getClass()), pathInfo, HttpMethod.GET );
      }
      return builder;
   }
  
   public void load(DataReader<BaseModel, List<BaseModel>> reader, 
       final BaseModel parent, final AsyncCallback<List<BaseModel>> callback) {
      
      RequestBuilder builder = getRequestBuilder( parent );
      // If the builder is null, then we do not have a Restful Builder we can handle.
      if( builder == null ) {
         callback.onSuccess(new ArrayList<BaseModel>());
         return;
      }
      builder.setCallback(new RequestCallback() {
      
         public void onError(Request request, Throwable exception) {
            GWT.log("error",exception);
         }

      @SuppressWarnings("unchecked")
      public void onResponseReceived(Request request, Response response) {
         if( response.getStatusCode() == Response.SC_OK ) {
            if (HttpUtils.isJsonMimeType(response.getHeader(HttpUtils.HEADER_CONTENT_TYPE))) {
               JSONValue json = JSONParser.parse(response.getText());
               Class _c = StampCollection.class;
               if( parent instanceof StampCollection || parent instanceof Album) {
                  _c = ( parent instanceof StampCollection ) ? Album.class : Country.class;
               }

               // Modified JsonReader which can read structures of ModelType definitions.
               // For this, I believe a regular JsonReader would work, however
               // it would create instances of BaseModel objects instead of
               // my specific types (which have nice accessor methods).
               StructuredJsonReader<BaseListLoadConfig> reader = new
               StructuredJsonReader<BaseListLoadConfig>(new StampModelTypeHelper(), _c );
               ListLoadResult lr = reader.read(new BaseListLoadConfig(), json.toString());
               callback.onSuccess(lr.getData());
            }
            } else {
               GWT.log("a non-status code of OK" + response.getStatusCode(), null);
            }
         }
      });
      try {
         builder.send();
      } catch (RequestException e) {
         e.printStackTrace();
      }
   }
}

Using this DataProxy, I can create an instance and pass it to the store I am creating for the tree:
public class BrowseStore extends TreeStore<BaseModel> {
  
   public BrowseStore( ) {
      super(new BaseTreeLoader<BaseModel>( new StampTreeDataProxy()));
   }
 
   // Simplified method to create the tree content and load it 
   public void load( ) {
      removeAll();
      getLoader().load();
   }
  
}

In this desgin, when load() is called (either on the loader of the store or the store itself), the DataProxy will be called for the root element. Since I am ignoring the reader (argument to the load( ) method) I create the new StructuredJsonReader. As stated above, the reason for this is three fold:
  1. It handles structured ModelTypes (ie. a ModelType with a field representing another ModelType.
  2. The ability to delegate the creation of the BaseModel to another class (in this case the StampModelTypeHelper which will create the appropriate instance of the modeled object (eg. Country).
  3. Finally uses the class provided to create the propert model type definition.

There is one negative of this solution. Currently it is posting a single request per node in the tree (eg. given an Album get all the countries). I plan on redesigning this to be a little more efficient, however given the mixture of BaseModel types, in order to get an efficient structure downloaded, I may need to forgoe the clean model structure in preference for a more efficient algorithm.

1 comment:

sdodger said...

Nice post I had the same problem. Though I could not realize the whole mechanism behind the expand and load of the tree. What is the cause and what is the root.

In the proxy when overriding load method. there was a BaseModel parent argument. From where did you get it?