Spring 3 MVC and optional relationships

Last December Ben Alex and his team released Spring Roo 1.0.0.GA. I’ve decided to give it a try. Even though there was some initial criticism I found Roo to be a quite useful tool.

After creating some CRUD controllers I wanted to enable optional relationships in the view (i.e. comboboxes with a null/empty option). As it’s quite a commonly requested feature and it cannot (at least as to my best knowledge)┬ábe enabled with toggling a single switch I’ve decided to share here my recipe how to do this.

Let’s say we have Node entity that can reference another node (it’s parent).

Node.java:

@Entity
@RooJavaBean
@RooToString
@RooEntity(identifierType = Integer.class)
public class Node {

	@ManyToOne
	@JoinColumn(name = "parent_id")
	private Node parent;

	// ...
}

Now let’s add the No parent option to the fragment holding the node parents list in the edit form JSP template:

<form:select cssStyle="width:250px" id="_parent_id" path="parent">
    <form:option value="" label="No parent" />
    <form:options itemValue="id" items="${nodes}" itemLabel="fullPath" />
</form>

After the form is submitted it reaches the NodeController.update() method:

@RequestMapping(method = RequestMethod.PUT)
    public String NodeController.update(@Valid Node node, BindingResult result, ModelMap modelMap) {
    // ...
    }

But before this happens the values that were submitted have to be converted into proper Node properties. The id into an Integer, date time values into a java.util.Date objects, etc. Without going into further details this happens in Spring’s conversion system.
All entity beans by default hit the org.springframework.core.convert.support.IdToEntityConverter converter. The converter basically tries to find a findEntityName() static method on the class that it has to convert to and call it passing the id from the HTML form. It does that regardless whether the id is null or not. And since the default Roo finders throw an IllegalArgumentException, as shown bellow, we quickly see the “Internal Error” page and a long stack trace in the server log.

    public static Node Node.findNode(Integer id) {
        if (id == null) throw new IllegalArgumentException("An identifier is required to retrieve an instance of Node");
        return entityManager().find(Node.class, id);
    }

Here are the two first options for solving this that came to my mind:

  • We can push in all the Roo finders from aspect files into the respective Java classes and change their behaviour. But it’s not the most elegant solution and, yes, it changes the finder behaviour, which might not be desirable.
  • Write specific converters for every entity class in our project.

Neither of these seemed to me a good solution. So I looked into tweaking the Spring the default spring converter. The new IdToEntityOrNullConverter has basically only 2 lines added in the convert() method:

	public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
		Method finder = getFinder(targetType.getType());
		Object id = this.conversionService.convert(source, sourceType, TypeDescriptor.valueOf(finder
				.getParameterTypes()[0]));
		if (id == null)
			return null;

		return ReflectionUtils.invokeMethod(finder, source, id);
	}

And it works!

You need to remember to register the beans in the applicationContext.xml as well:

	<mvc:annotation-driven conversion-service="conversionService" />
	<!--
		There are two conversion services. The generic one is used by idToEntityOrNullConverter as it requires one. We cannot
		use the conversionService that is being used in the MVC context, as we would run into the chicken-and-egg problem
		trying to instantiate the converter and the conversionService.
	-->
	<bean id="genericConversionService" class="org.springframework.context.support.ConversionServiceFactoryBean" />

	<bean id="idToEntityOrNullConveter" class="org.inszy.spring.support.IdToEntityOrNullConveter">
		<constructor-arg index="0" ref="genericConversionService" />
	</bean>

	<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
		<property name="converters">
			<list>
				<ref bean="idToEntityOrNullConveter" />
			</list>
		</property>
	</bean>

There is also a JIRA issue available for this problem #ROO-581.

IdToEntityOrNullConverter.java listing:

package org.inszy.spring.support;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.Set;

import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

public final class IdToEntityOrNullConveter implements ConditionalGenericConverter {

	private final GenericConversionService conversionService;

	public IdToEntityOrNullConveter(GenericConversionService conversionService) {
		this.conversionService = conversionService;
	}

	public Set getConvertibleTypes() {
		return Collections.singleton(new ConvertiblePair(Object.class, Object.class));
	}

	public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
		Method finder = getFinder(targetType.getType());
		return (finder != null && this.conversionService.canConvert(sourceType, TypeDescriptor.valueOf(finder
				.getParameterTypes()[0])));
	}

	public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
		Method finder = getFinder(targetType.getType());
		Object id = this.conversionService.convert(source, sourceType, TypeDescriptor.valueOf(finder
				.getParameterTypes()[0]));
		if (id == null)
			return null;

		return ReflectionUtils.invokeMethod(finder, source, id);
	}

	private Method getFinder(Class< ?> entityClass) {
		String finderMethod = "find" + getEntityName(entityClass);
		Method[] methods = entityClass.getDeclaredMethods();
		for (Method method : methods) {
			if (Modifier.isStatic(method.getModifiers()) && method.getParameterTypes().length == 1
					&& method.getReturnType().equals(entityClass)) {
				if (method.getName().equals(finderMethod)) {
					return method;
				}
			}
		}
		return null;
	}

	private String getEntityName(Class< ?> entityClass) {
		String shortName = ClassUtils.getShortName(entityClass);
		int lastDot = shortName.lastIndexOf('.');
		if (lastDot != -1) {
			return shortName.substring(lastDot + 1);
		} else {
			return shortName;
		}
	}

}

Any feedback is greatly appreciated.

UPDATE 2010.02.12: This should no longer be required if you’re using Roo 1.0.2 (or newer) as Andrew Swan noted on the Spring forums.

One thought on “Spring 3 MVC and optional relationships

  1. I m a spring user since years, and i was an early grails adopter… i discover roo recently and i think it might be great, but i really wonder about spring roadmap grails and roo seams really competitor !

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s