jeudi 12 novembre 2009

Using gwt-dispatch without GWT RPC


I watched the Best Practices For Architecting Your GWT App video, and followed the excellent GWT MVP Example tutorial.

And it gave my some ideas on how to implement the command pattern in my GWT application.

The pattern is nice in a GWT app:
  • We have a centralized component to handle cross-cutting concerns like caching, logging, handling network errors, etc.
  • The components are no longer aware of the implementation details of the client / server communication, so:
    • Better unit testing: Handlers can be easily mocked
    • Handlers implementation could be swapped with minimal impact to:
      • change the client / server communication: For example change JSON to GWT RPC
      • or why not while the application is running: For example to implement an offline mode, the JSON handlers would be swapped to Gears handlers, without the components noticing.

I wanted to use the gwt-dispatch lib, which implements this pattern in GWT, problem is: At the time of this writing, in gwt-dispatch, handlers could only be implemented on the server and with GWT RPC.
And in my application the client / server communication is done with SOAP.

So I hacked a couple of classes to handle the commands on the client side:
  • RequestBuilderDispatcher, a implementation of DispatchAsync, capable of executing RequestBuilderActionHandlers, 2 public methods:
    • addHandler : To add a RequestBuilderHandler for a specific Action
    • execute: To execute an Action with a RequestBuilderActionHandler.
  • RequestBuilderActionHandler : All handlers must implements this interface.  2 methods are defined:
    • getRequestBuilder: Prepare and configure a RequestBuilder with the content of the Action: Setup the url, post method, request parameters, ...
    • extractResult: Extract a Result from the server Response.

RequestBuilderActionHandler.java

package fr.hrgwt.client.command;

 

import net.customware.gwt.dispatch.shared.Action;

import net.customware.gwt.dispatch.shared.Result;

 

import com.google.gwt.http.client.RequestBuilder;

import com.google.gwt.http.client.Response;

 

public interface RequestBuilderActionHandler<A extends Action<R>, R extends Result> {

 

              /**

              * Prepare a RequestBuilder.

              */

              RequestBuilder getRequestBuilder(A action);

 

              /**

              * Extract the result from the response.

              */

              R extractResult(Response response);

 

              Class<A> getActionType();

}


Here an example of implementation:

FindAllCheckListXmlActionHandler.java

package fr.hrgwt.client.command.impl; 


import com.google.gwt.http.client.RequestBuilder;

import com.google.gwt.http.client.Response;

import fr.hrgwt.client.command.RequestBuilderActionHandler;

 

public class FindAllCheckListXmlActionHandler

                            implements

                            RequestBuilderActionHandler<FindAllCheckListAction, FindAllCheckListActionResult> {

 

              @Override

              public Class<FindAllCheckListAction> getActionType() {

                            return FindAllCheckListAction.class;

              }

 

              @Override

              public RequestBuilder getRequestBuilder(FindAllCheckListAction action) {

                            return new RequestBuilder(RequestBuilder.GET, "/myurl");

              }

 

              @Override

              public FindAllCheckListActionResult extractResult(Response response) {

                            // TODO extract the xml from the response here

                            return null;

              }

}


Code for RequestBuilderDispatcher:

RequestBuilderDispatcher.java

package fr.hrgwt.client.command;

 

import java.util.HashMap;

import java.util.Map;

 

import net.customware.gwt.dispatch.client.DispatchAsync;

import net.customware.gwt.dispatch.shared.Action;

import net.customware.gwt.dispatch.shared.Result;

 

import com.google.gwt.http.client.Request;

import com.google.gwt.http.client.RequestBuilder;

import com.google.gwt.http.client.RequestCallback;

import com.google.gwt.http.client.RequestException;

import com.google.gwt.http.client.Response;

import com.google.gwt.user.client.rpc.AsyncCallback;

 

/**

* Implements DispatchAsync with a RequestBuilder.

*/

public class RequestBuilderDispatcher implements DispatchAsync {

              @SuppressWarnings("unchecked")

              Map<Class<Action>, RequestBuilderActionHandler> handlersMap = new HashMap<Class<Action>, RequestBuilderActionHandler>();

 

              @SuppressWarnings("unchecked")

              public <A extends Action<R>, R extends Result> void execute(A action,

                                          final AsyncCallback<R> asyncCallback) {

                            Class<? extends Action> actionType = action.getClass();

                            final RequestBuilderActionHandler<A, R> handler = handlersMap

                                                        .get(actionType);

                            if (handler == null) {

                                          throw new IllegalArgumentException("unregistered actionType:"

                                                                      + actionType);

                            }

                            RequestBuilder requestBuilder = handler.getRequestBuilder(action);

                            requestBuilder

                                                        .setCallback(createRequestCallback(asyncCallback, handler));

                            try {

                                          requestBuilder.send();

                            }

                            catch (RequestException e) {

                                          throw new RequestRuntimeException(e);

                            }

              }

 

              // visibility package for tests

              <A extends Action<R>, R extends Result> RequestCallback createRequestCallback(

                                          final AsyncCallback<R> asynchCallback,

                                          final RequestBuilderActionHandler<A, R> handler) {

 

                            return new RequestCallback() {

 

                                          @Override

                                          public void onResponseReceived(Request request, Response response) {

                                                        asynchCallback.onSuccess(handler.extractResult(response));

                                          }

 

                                          @Override

                                          public void onError(Request request, Throwable exception) {

                                                        throw new RuntimeException("not implemented yet");

                                          }

                            };

              }

 

              @SuppressWarnings("unchecked")

              public <A extends Action<R>, R extends Result> void addHandler(

                                          RequestBuilderActionHandler<A, R> handler) {

                            handlersMap.put((Class<Action>) handler.getActionType(), handler);

              }

}



Now in the initialization of the app:
  • Create a dispatcher that will be injected in the components that needs it (manually or better with GIN)
  • Register an ActionHandler

                            RequestBuilderDispatcher dispatcher = new RequestBuilderDispatcher();

                            dispatcher.addHandler(new FindAllCheckListXmlActionHandler());


Nothing special when using the dispatcher (the client code won't know anything about the implementation, it could be GWT RPC) :

              @Override

              protected void onBind() {

                            dispatcher.execute(new FindAllCheckListAction(),

                                                        new AsyncCallback<FindAllCheckListActionResult>() {

 

                                                                      @Override

                                                                      public void onSuccess(FindAllCheckListActionResult result) {

                                                                                    display.setCheckLists(result.getCheckLists());

                                                                      }

 

                                                                      @Override

                                                                      public void onFailure(Throwable caught) {

                                                                                    throw new RuntimeException("not implemented yet");

                                                                      }

                                                        });

Conclusion

The good

  • Quick implementation of an ActionHandler on the client side.

The bad

  • Rollback is not handled
  • No GIN integration

Remarks

  • My build is dependant on the gwt-dispatch library, this is not really necessary as I am only using 3 interfaces from it (Action, Result and AsyncDispatch). It would be interessting to see if there's an impact on the size of the compiled Javascript modules.

Membres