Friday

Custom DWR Converters

DWR is great. If you're full bore into Ajax development, it eliminates the need for a awkward controller like JSF or Spring MVC. You can write static DHTML that talks directly to your service layer, which keeps the UI code clear and simple. Secure your services with something like Acegi, and you're almost there.

I say almost, because the way DWR marshals your beans is either all or nothing. You can tell DWR, convert only these classes, and only these properties and that's it. What if you have different user roles that have different read permissions? e.g., detailed user info should be accessible to the admin and no one else. What if you want summary information when an object is nested inside another one? e.g., my Label BO has a name, description and a couple of flags associated with it. When I retrieve a labeled thing, say a message, I really only care about the label name so I can render a list of labels on the message. But when I retrieve a Label for editing, I need all the information about the label. Let's see if we can fix this.

Because DWR is open source, I can browse around and find the source for BeanConverter. It has a lot of hooks (including isAllowedByIncludeExcludeRules, which would be helpful for role based filtering), but what we really need here is a hook to control what converter is used. Because we have the source code, why don't we add one? I'll just create a subclass of BeanConverter and override convertOutbound like so:

public OutboundVariable convertOutbound(Object data, OutboundContext outctx)
throws MarshallException {
Map ovs = new TreeMap();

ObjectOutboundVariable ov = new ObjectOutboundVariable(outctx);
outctx.put(data, ov);

try {
for (Entry entry : (Set>) getPropertyMapFromObject(
data, true, false).entrySet()) {
ovs.put((String) entry.getKey(), getPropertyValue(
(Property) entry.getValue(), data, outctx));
}
} catch (MarshallException ex) {
throw ex;
} catch (Exception ex) {
throw new MarshallException(data.getClass(), ex);
}

ov.init(ovs, getJavascript());

return ov;
}

The code is pretty much the same as in the superclass, but with 1 key difference. My version calls getPropertyValue. In the template class, I keep the default behavior, loading the converter from the converter manager, but we'll create a subclass in a moment that takes a different approach.

First, let's look at what dwr.xml offers us in terms of configuration. It looks like you can set properties on converter instances using param elements. I prefer to use a concise format for configuration whenever possible, so our format will be something like "propertyName:includeExcludeProperty1,includeExcludeProperty2 otherProp:more,of,the,same". Supposing this was an include, then when we create the converter for the otherProp property, it will include the more,of,the and same properties and no others. Got it?

We need to provide parsing for our custom format in the converter:

public class SubBeanConverter extends TemplateBeanConverter {

protected Map subInclude = new HashMap();
protected Map subExclude = new HashMap();

public void setSubIncludes(String defs) {
this.subInclude = parseIncludeExclude(defs, true);
}

public void setSubExcludes(String defs) {
this.subExclude = parseIncludeExclude(defs, false);
}

private Map parseIncludeExclude(String defs,
boolean include) {
Map map = new HashMap();
for (String def : defs.split("\\s")) {
String[] split = def.split(":", 2);
BeanConverter bc = new BeanConverter();
bc.setConverterManager(getConverterManager());
if (include) {
bc.setInclude(split[1]);
} else {
bc.setExclude(split[1]);
}
map.put(split[0].trim(), bc);
}
return map;
}
...

TemplateBeanConverter is our original subclass of BeanConverter. Since we're using the same format for includes and excludes, the same method is used to parse them both. For each property we list, we create a BeanConverter instance that we will use in place of the usual converter.

The last step is to override getPropertyValue and use our special BeanConverter instances:

protected OutboundVariable getPropertyValue(Property property, Object data,
OutboundContext outctx) throws MarshallException {
Object value = property.getValue(data);
if (value != null) {
BeanConverter exConverter = subExclude.get(property.getName());
if (exConverter != null) {
return exConverter.convertOutbound(value, outctx);
}
BeanConverter inConverter = subInclude.get(property.getName());
if (inConverter != null) {
return inConverter.convertOutbound(value, outctx);
}
}
return super.getPropertyValue(property, data, outctx);
}

Easy right?

Next time I'll talk about my experience using Grails with DWR.

UPDATE: I mentioned role based filtering above, but I will leave that as an exercise to the reader. One hint: if you're using Acegi to control access to pages,
org.acegisecurity.context.SecurityContextHolder.getContext()
.getAuthentication().getAuthorities()
will get you the list of granted authorities (AKA roles, e.g. ROLE_ANONYMOUS) for the current user.