Custom event dispatchers in Apache Wicket, part 2

In part 1 we created our own custom event dispatcher for Wicket. In part 2 we will make it simpler to use for developers, faster and usable in more cases. The text in part 2 is written under the assumption that the reader has read part 1, so many things will not be explained here.

The main problem of our previous implementation is that we need to both annotate the class and the method. And also we need to specify the payload we are interested in for both annotations. There are several problems with this:

  • If we forget the annotation on the class then the annotation on the method does nothing
  • If we forget a specific payload on the class annotation it will never reach the method handling it
  • In Wicket it is common to create anonymous inner classes, but since we require an annotation on the class we can’t use our event dispatcher for such classes

So it seems like the obvious way is to remove the @EventPayloadReceiver annotation for the class and only keep the @OnEventPayload annotation for the method. But we added the class annotation to optimize event delivery, so we didn’t have to check each method in each component for every event delivered. So how do we keep everything running fast while removing the class annotation? Well by caching of course.

So what we will do is keep the @OnEventPayload as it is, delete the @EventPayloadReceiver and rewrite the event delivery in our dispatcher AnnotationPayloadEventDispatcher. All the code following is in our event dispatcher.

First we create a static Map to hold the cached data:

private final static Map<Class<?>, Map<Class<?>, MethodInfo>> cacheMap;
static {
cacheMap = new HashMap<Class<?>, Map<Class<?>, MethodInfo>>();
}

The MethodInfo in the Map is a simple class that describes the basics needed to call a specific method:

private static class MethodInfo {
private final String name;
private final Class[] parameterTypes;
  /**
* @param name name of the method
* @param parameterTypes the parameter type list of the method
*/
private MethodInfo(String name, Class[] parameterTypes) {
this.name = name;
this.parameterTypes = parameterTypes;
}
}

Then we create a few simple helper methods for working with the cache Map:

/**
* Checks the cache if a sink has been cached.
*
* @param sinkClass class of the sink
* @return true if cached
*/
private boolean isSinkRegistered(Class sinkClass) {
return cacheMap.containsKey(sinkClass);
}
/**
* Checks both that the sink has been cached and that it accepts a
* specific payload.
*
* @param sinkClass the class of the sink
* @param payloadClass the class of the payload
* @return true if both cached and it accepts the payload
*/
private boolean isSinkRegisteredForPayload(Class sinkClass, Class payloadClass) {
return isSinkRegistered(sinkClass) && cacheMap.get(sinkClass).containsKey(payloadClass);
}
/**
* Gets info from the cache about a method that has the
* OnEventPayload annotation.
*
* @param sinkClass the sink that contains the method
* @param payloadClass the payload that we want the corresponding
* method for
* @return info about the method */
private MethodInfo getMethodInfo(Class sinkClass, Class payloadClass) {
return cacheMap.get(sinkClass).get(payloadClass);
}

The next step is writing a helper method that does the actual caching. This method will cache all methods in a sink that are annotated with the @OnEventPayload annotation and the payloads it is configured to accept:

private void cacheSink(Object sink) {
Map<Class<?>, MethodInfo> payloadMethodMap = new HashMap<Class<?>, MethodInfo>();
  // Go through all methods and check for the annotation
for (Method method : sink.getClass().getDeclaredMethods()) {
if (method.isAnnotationPresent(OnEventPayload.class)) {
OnEventPayload onEvent = method.getAnnotation(OnEventPayload.class);
      // Go through all payloads in the annotation and update the
// cache
for (Class payloadClass : Arrays.asList(onEvent.value())) {
if (!payloadMethodMap.containsKey(payloadClass)) {
payloadMethodMap.put(payloadClass, new MethodInfo(method.getName(), method.getParameterTypes()));
}
}
}
}
  cacheMap.put(sink.getClass(), payloadMethodMap);
}

Then we will write a helper method that handles the delivery of the event payload to a sink by using reflection:

private void deliverEvent(Object sink, IEvent event, MethodInfo methodInfo) {
try {
// Get the method by using the MethodInfo data
Method method = sink.getClass().getMethod(methodInfo.name, methodInfo.parameterTypes);
    // Make it accessible if it isn’t
if (!method.isAccessible()) {
method.setAccessible(true);
}
    // Invoke the method and deliver the event payload
method.invoke(sink, event.getPayload());
} catch (Exception e) {
throw new RuntimeException(“Exception when delivering event object “ + event.getPayload().getClass() + “ to component “ + sink.getClass() + “ and method “ + methodInfo.name, e);
}
}

Now we have all the helper methods we need and we can rewrite the main method, dispatchEvent, of the event dispatcher:

@Override
public void dispatchEvent(Object sink, IEvent event, Component component) {
// The payload must not be null
if (event.getPayload() != null) {
// Check that the sink is cached, otherwise scan it and add it
// to the cache
if (!isSinkRegistered(sink.getClass())) {
cacheSink(sink);
}
    // Check if this sink accepts the specific payload, and deliver
// the event if it does
if (isSinkRegisteredForPayload(sink.getClass(), event.getPayload().getClass())) {
MethodInfo methodInfo = getMethodInfo(sink.getClass(), event.getPayload().getClass());
deliverEvent(sink, event, methodInfo);
}
}
}

And that is it. Now we have an implementation with only one annotation and one event dispatcher which makes using it much simpler. On top of that we have a cache, that is shared among all users, which will make it faster and we can use this event system in anonymous inner classes. So the benefits are many and the only real drawback is that we have a little bit more code in the event dispatcher.