I develop a client for a logistics system in GWT, using Mvp4g (currently GWT 2.3 with Mvp4g 1.3.1). The system has technically quite complex integration tests, where a J5EE based (Glassfish 2.1 + Seam 2.2) core application serves multiple WPF clients (with web services using WCF and Metro) and multiple GWT web clients. I use the same infrastructure to test the overall performance of the system. It might not have been the best decision, but I didn't want to use Selenium at that time, and HtmlUnit had (maybe still has) some issues with my application which I didn't want to sort out (although it should work), so I chose to instantiate my GWT application in the JVM, using mock views. The main application code is in the presenters anyway, so it should be easy to use from a JVM. Well, not that easy, but not a catastrophe.
Presenters and the EventBus
So, presenters should be instantiable without modification in the JVM. Any GWT UI related code should be in the views, that's not a big restriction. The EventBus itself is generated at compile time by Mvp4g, and I didn't want to use those generators, probably the generated code runs only in a browser, due to the use of Guice, but maybe I'm wrong :).
Anyway, the EventBus is quite simple to implement with reflection and dynamic proxies. What we have to do is basically:
- contain an instance of each presenter, and bind them to their views,
- maintain a list of presenters handling each event
- and delegate each event method invocation to those presenters, using the method name convetion.
So here is the code I use:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import com.google.inject.Injector;
import com.mvp4g.client.annotation.Event;
import com.mvp4g.client.annotation.InitHistory;
import com.mvp4g.client.event.EventHandlerInterface;
import com.mvp4g.client.presenter.PresenterInterface;
public class EventBus<T extends com.mvp4g.client.event.EventBus> {
Class<T> interfaceClass;
T eventBus;
@SuppressWarnings("unchecked")
Map<Class<PresenterInterface>, PresenterInterface> presenters = new HashMap<Class<PresenterInterface>, PresenterInterface>();
Map<String, EventDescriptor> events = new HashMap<String, EventDescriptor>();
EventDescriptor initEvent;
Injector injector;
@SuppressWarnings("unchecked")
public EventBus(Class<T> interfaceClass, Injector injector) {
this.injector = injector;
this.interfaceClass = interfaceClass;
eventBus = (T) Proxy.newProxyInstance(EventBus.class.getClassLoader(),
new Class [] { interfaceClass }, new EventBusInvocationHandler());
for (Method method : interfaceClass.getMethods())
if (method.isAnnotationPresent(Event.class)) {
Event event = method.getAnnotation(Event.class);
EventDescriptor eventDescriptor = new EventDescriptor();
eventDescriptor.method = method;
eventDescriptor.eventName = method.getName();
eventDescriptor.targetMethodName = "on" + Character.toUpperCase(eventDescriptor.eventName.charAt(0)) + eventDescriptor.eventName.substring(1);
for (Class<? extends EventHandlerInterface> cls : event.handlers())
eventDescriptor.handlers.add(getPresenter(cls));
events.put(eventDescriptor.eventName, eventDescriptor);
if (method.isAnnotationPresent(InitHistory.class))
initEvent = eventDescriptor;
}
}
@SuppressWarnings("unchecked")
public void bindView(Class<? extends PresenterInterface> presenterClass, Object view) {
PresenterInterface presenter = getPresenter(presenterClass);
presenter.setView(view);
presenter.setEventBus(eventBus);
presenter.bind();
}
public void init() {
try {
initEvent.method.invoke(eventBus);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
public <T extends EventHandlerInterface> T getPresenter(Class<T> presenterClass) {
T presenter = (T) presenters.get(presenterClass);
if (presenter == null) {
try {
presenter = injector.getInstance(presenterClass);
presenters.put((Class<PresenterInterface>) presenterClass, (PresenterInterface) presenter);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return presenter;
}
public T getEventBus() {
return eventBus;
}
class EventDescriptor {
List<EventHandlerInterface> handlers = new Vector<EventHandlerInterface>();
String eventName;
String targetMethodName;
Method method;
}
class EventBusInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("hashCode".equals(method.getName())) {
return EventBus.this.hashCode();
}
EventDescriptor eventDescriptor = events.get(method.getName());
for (EventHandlerInterface presenter : eventDescriptor.handlers) {
Method presenterMethod = presenter.getClass().getMethod(eventDescriptor.targetMethodName, eventDescriptor.method.getParameterTypes());
presenterMethod.invoke(presenter, args);
}
return null;
}
}
}
The Application class
In Mvp4g you define the entry point (or use the mvp4g built in entry point) to bootstrap the framework. In JVM, we create an Application class that does the initialization.
While Mvp4g uses Gin, in the JVM we use Guice to do the injection stuff. There's nothing special to it, if you had a GinModule in Mvp4g, you can create a GuiceModule, and provide its injector to the EventBus above.
The Application contains and instantiates the GuiceModule, the EventBus and the mock views, and then calls EventBus#bindView method to give the views to the EventBus.
That's almost all, now you are able to instantiate your application, and play with it through the mock views. You might also directly call events from your test code.
GWT Service invocations
Well, if your application calls GWT services as well, you have to do some hacking about it. The GWT RPC implementation is not symmetric, which means that for example the Readers/Writers (Marshallers) are different on the client and server side. The stream written with a server side writer can only be read by a reader on the client side, not on the server side (classes: com.google.gwt.user.server.rpc.impl.(Client|Server)SerializaionStream(Writer|Reader) ).
Fortunately I wasn't the first to want to call a GWT RPC service from JVM, and there's a project called gwt-syncproxy. It can create sync and async proxies for you as well. I forked in my local workspace and added some functionality to support performance monitoring transparently (see below).
There was still a small issue. As I use this stuff from test code, I have to make sure that all async service invocations finish before I do my assertions in my tests. To achieve this, I extended gwt-syncproxy a little further, and added a little code that keeps track of invocations in each thread (and 'child'-threads), so that the test code can call a waitForInvocations() method before going on to the assertions.
I18n
When I added i18n to the application, my tests suddenly failed :) . It was because while in GWT, it creates and implementation of the message interface, in the JVM we have to replace it with something. Anyway, the localized strings are not so important during the tests, I don't call assertions textual content. However, there has to be an object with the interface. I love creating proxies, so here it is:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.google.gwt.i18n.client.LocalizableResource.Key;
import com.google.gwt.i18n.client.Messages.DefaultMessage;
/**
* A class to implement com.google.gwt.i18n.client.Messages derived interfaces when running in a JVM
*/
public class MessagesFactory {
public static <T extends com.google.gwt.i18n.client.Messages> T createInstance(Class<T> cls) {
return (T) Proxy.newProxyInstance(MessagesFactory.class.getClassLoader(), new Class [] { cls },
new MessagesInvocationHandler(cls));
}
protected static class MessagesInvocationHandler<T extends com.google.gwt.i18n.client.Messages>
implements InvocationHandler {
protected Class<T> cls;
public MessagesInvocationHandler(Class<T> cls) {
this.cls = cls;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.isAnnotationPresent(DefaultMessage.class)) {
return method.getAnnotation(DefaultMessage.class).value();
}
if (method.isAnnotationPresent(Key.class)) {
return method.getAnnotation(Key.class).value();
}
return method.getName();
}
}
}
And in the Guice module, I have to bind it manually:
bind(MyMessages.class).toInstance(MessagesFactory.createInstance(MyMessages.class));
Performance logging
Once I got the taste of using and extending everything in strage ways, I also added performance monitoring on the client side for GWT RPC invocations.
To proxy requests on the client side, I found a solution by Nathan Williams. You can declare in your gwt.xml for which service interfaces you want to use the proxy and then use the bind method when creating the service to pass it an AsyncInvocationHandler that will be called before the actual invocation, and on success and failure. I also extended the gwt-syncproxy in my workspace to support these invocation handlers, and thus I can have my performance data from the integration and load tests too.