27 August 2009

Integrating gwt-dispatch with JBoss Seam (and probably Spring!)

In my last post I gave an example based on the Google Webtool Kit (GWT) Starter Application using MVP, dependency injection, event bus and command patterns. The command pattern made use of the excellent gwt-dispatch library and used Google Guice at the server side for wiring the service handlers together. My current project uses JBoss Seam for implementing services and this requires a slightly different configuration so that the server side command handlers are properly injected with Seam managed components.

Therefore the aim of this post is to demonstrate those changes involved so you can integrate your own gwt-dispatch based applications with Seam. I expect that most of the changes here will also apply to services implemented using other server frameworks such as Spring. Naturally, you'll need to appropriately tailor the steps for the Seam specific parts but it should serve as a good starting point nonetheless.

Note 1: I should also point out that the code here demonstrates the bare minimum to get the client talking to GWT and does not take into account how security or validation exceptions from the Seam framework might be handled etc.

Note 2: JBoss Seam does in fact support integration with Guice 1.0. However, since I am writing code from fresh rather than integrating with existing code I feel it is better to produce the command handlers as first class Seam citizens.

I'm assuming that you already have a Seam project configured to talk to GWT via regular RPC (if not, then see the Seam docs on how to do this), you've got the code from the MVP example copied into the project and wish to talk to Seam services instead of the Guice versions.

The approach we'll take is to create our own implementation of the command (a.k.a. Action) dispatch mechanism using standard RPC calls to Seam and then extend the gwt-dispatch default dispatcher implementation to create an ActionHandler instance that has been suitably wrapped by Seam's interceptors. Once this boiler plate code is in place, the original Action and Result code and (more importantly) the calls to them remain unchanged while the action handlers can be modified to use Seam components.

Changes to the Client

  • 1. Define a RPC service to process our commands;
  • 2. Point the client to Seam friendly URLs;
  • 3. Wire it all up.

First up, we need to define an RPC call to carry the command to the server using standard RPC:

DispatchService.java

package co.uk.hivedevelopment.greet.client.gin;

import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;
import com.google.gwt.user.client.rpc.RemoteService;

public interface DispatchService extends RemoteService {

Result execute(Action<?> action) throws Exception;
}
DispatchServiceAsync.java

package co.uk.hivedevelopment.greet.client.gin;

import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;
import com.google.gwt.user.client.rpc.AsyncCallback;

public interface DispatchServiceAsync {

void execute(Action<?> action, AsyncCallback<Result> callback);
}

Now we need to change the RPC service URL to a Seam friendly one. By default, gwt-dispatch will look to go to a servlet located at the URL http://<host>/<context>/dispatcher. Seam requires the URL point to http://<host>/<context>/seam/resource/gwt.

SeamDispatchAsync.java

package co.uk.hivedevelopment.greet.client.gin;

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.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;

public class SeamDispatchAsync implements DispatchAsync {

private static final DispatchServiceAsync realService;

static {
realService = GWT.create( DispatchService.class );
final String endpointURL = getModuleBaseURL() + "seam/resource/gwt";

((ServiceDefTarget) realService).setServiceEntryPoint(endpointURL);
}

public static String getModuleBaseURL() {
// Make sure that communication is with the server that served the containing
// web page and not where the GWT resources came from (which is the case with
// GWT.getHostPageBaseURL)
final String url = GWT.getHostPageBaseURL();

return url;
}

public SeamDispatchAsync() {
}

public <A extends Action<R>, R extends Result> void execute( final A action, final AsyncCallback<R> callback ) {
realService.execute(action, new AsyncCallback<Result>() {

public void onFailure(final Throwable caught) {
callback.onFailure( caught );
}

@SuppressWarnings("unchecked")
public void onSuccess(final Result result) {
callback.onSuccess((R) result);
}
} );
}
}

All standard RPC stuff so far. Finally, to wire everything together we define a new Gin module called SeamDispatchModule and modify the annotation @GinModules on the GreetingGinjector class to use the new SeamDispatchModule rather than the default ClientDispatchModule:

SeamDispatchModule.java

package co.uk.hivedevelopment.greet.client.gin;

import net.customware.gwt.dispatch.client.DispatchAsync;
import com.google.gwt.inject.client.AbstractGinModule;
import com.google.inject.Singleton;

public class SeamDispatchModule extends AbstractGinModule {

@Override
protected void configure() {
bind( DispatchAsync.class ).to( SeamDispatchAsync.class ).in( Singleton.class );
}
}
GreetingGinjector.java

package co.uk.hivedevelopment.greet.client.gin;

import net.customware.gwt.presenter.client.place.PlaceManager;
import com.google.gwt.inject.client.GinModules;
import com.google.gwt.inject.client.Ginjector;
import co.uk.hivedevelopment.greet.client.mvp.AppPresenter;

@GinModules({ SeamDispatchModule.class, GreetingClientModule.class })
public interface GreetingGinjector extends Ginjector {

AppPresenter getAppPresenter();

PlaceManager getPlaceManager();
}

That's it for the client. The existing GreetMvp code was written to use dispatcher interfaces so once we gave Gin an alternative implementation anywhere that uses the dispatcher is injected with new version. Ahhh, good old dependency injection.

Configuration for the Server

Under the GreetMvp example application, all code under the co.uk.hivedevelopment.greet.server.guice package is no longer required. Also if you were already making GWT RPC calls to Seam then there are no changes necessary to your existing Seam web.xml or components.xml. Just make sure that gwt-dispatch jars (aopalliance.jar and gwt-dispatch-1.0.0-SNAPSHOT.jar) are deployed with the application WAR file (i.e. add them to your Eclipse project and edit deployed-jars.list).

At the server we'll:

  • Define the server RPC dispatch implementation;
  • Create a Seam registry for handlers;
  • Modify the SendGreetingHandler for Seam injection.

The following is a standard Seam RPC end point. Notice how the component name matches the fully qualified client side DispatchService remote service interface - this is a key part of any Seam GWT integration:

DispatchServiceImpl.java

package co.uk.hivedevelopment.greet.server.seam;

import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.remoting.WebRemote;
import co.uk.hivedevelopment.greet.client.gin.DispatchService;

@Name("co.uk.hivedevelopment.greet.client.gin.DispatchService")
public class DispatchServiceImpl implements DispatchService {

@In GwtActionDispatcher gwtActionDispatcher;

@WebRemote
@Override
public Result execute(final Action<? extends Result> action) throws Exception {
return gwtActionDispatcher.execute(action);
}
}
GwtActionDispatcher.java

package co.uk.hivedevelopment.greet.server.seam;

import net.customware.gwt.dispatch.server.DefaultDispatch;
import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.ActionException;
import net.customware.gwt.dispatch.shared.Result;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.AutoCreate;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import co.uk.hivedevelopment.greet.server.handler.SendGreetingHandler;

@Name("gwtActionDispatcher")
@Scope(ScopeType.APPLICATION)
@AutoCreate
public class GwtActionDispatcher {

private SeamActionHandlerRegistry actionHandlerRegistry;

@Create
public void init() {
actionHandlerRegistry = new SeamActionHandlerRegistry();

addHandlers();
}

private void addHandlers() {
// TODO: Add all your handlers here.
actionHandlerRegistry.addHandler(new SendGreetingHandler());
}

public Result execute(final Action<? extends Result> action) throws ActionException {
final DefaultDispatch dd = new DefaultDispatch(actionHandlerRegistry);

return dd.execute(action);
}
}
SeamActionHandlerRegistry.java

package co.uk.hivedevelopment.greet.server.seam;

import net.customware.gwt.dispatch.server.ActionHandler;
import net.customware.gwt.dispatch.server.DefaultActionHandlerRegistry;
import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;
import org.jboss.seam.Component;

public class SeamActionHandlerRegistry extends DefaultActionHandlerRegistry {

@SuppressWarnings("unchecked")
@Override
public <A extends Action<R>, R extends Result> ActionHandler<A, R> findHandler(final A action) {
final ActionHandler<A, R> handler = super.findHandler(action);

if (handler == null) {
return null;
}

// The crucial part to the Seam dispatch implementation is to create handlers using getInstance
final ActionHandler<A, R> handler_ = (ActionHandler<A, R>) Component.getInstance(handler.getClass());

return handler_;
}
}

That completes the server side boiler plate code. The main bit to focus on is the SeamActionHandlerRegistry which extends the default gwt-dispatch implementation to create handlers via Seam's getInstance() method which ensures that appropriate Seam bijection takes place. Remember to add all your new handlers to GwtActionDispatcher.addHandlers().

Finally, here is the version of SendGreetingHandler amended for Seam:

SendGreetingHandler.java

package co.uk.hivedevelopment.greet.server.seam;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import net.customware.gwt.dispatch.server.ActionHandler;
import net.customware.gwt.dispatch.server.ExecutionContext;
import net.customware.gwt.dispatch.shared.ActionException;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.log.Log;
import org.jboss.seam.web.ServletContexts;
import co.uk.hivedevelopment.greet.shared.rpc.SendGreeting;
import co.uk.hivedevelopment.greet.shared.rpc.SendGreetingResult;

@Name("sendGreetingHandler")
@Scope(ScopeType.STATELESS)
public class SendGreetingHandler implements ActionHandler<SendGreeting, SendGreetingResult> {

@Logger Log logger;
@In("org.jboss.seam.web.servletContexts") ServletContexts servletContexts;

@Override
public SendGreetingResult execute(final SendGreeting action,
final ExecutionContext context) throws ActionException {
final String name = action.getName();

try {
final HttpServletRequest request = servletContexts.getRequest();
final ServletContext sc = request.getSession().getServletContext();

final String serverInfo = sc.getServerInfo();
final String userAgent = request.getHeader("User-Agent");

final String message = "Hello, " + name + "! I am running " + serverInfo + ". It looks like you are using:" + userAgent;

return new SendGreetingResult(name, message);
}
catch (Exception cause) {
logger.error("Unable to send greeting", cause);

throw new ActionException(cause);
}
}

@Override
public void rollback(final SendGreeting action,
final SendGreetingResult result,
final ExecutionContext context) throws ActionException {
// Nothing to do here
}

@Override
public Class<SendGreeting> getActionType() {
return SendGreeting.class;
}
}

Good luck!