In had a similar problem (JSR-303 / Spring MVC - validate conditionally using groups)
The main idea of my solution is to dynamically bind the data, i.e. step by step conditionally bind and validate the input data:
I created a new annotation class @BindingGroup
. It's similar to the groups parameter of validation constraint annotations. In my solution you use it to specify a group of a field which has no validation constraint.
I created a custom binder called GroupAwareDataBinder. When this binder is called, a group is passed and the binder only binds fields which belong to this group. To set a group for a field you can use the new @BindingGroup
annotation. As there could be also situations where normal groups are sufficient, the binder also looks for the groups parameter of validation constraints. For convenience the binder provides a method bindAndValidate()
.
Specifiy a binding group called BasicCheck
and a second binding group AddressCheck
and assign them to the respective fields of your Person and Address class.
Now you can perform the binding of your data step by step in your controller method. Here is some pseudo code:
result = binder.getBindingResult();
binder.bindAndValidate(data, BasicCheck.class);
if (person.hasAddress)
binder.bindAndValidate(data, AddressCheck.class);
if (!result.hasErrors())
// do something
As you can see, the downside is that you have to perform the binding on your own instead of using nice annotations.
Here is my source code:
BindingGroup:
import java.lang.annotation.*;
@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BindingGroup
{
Class<?>[] value() default {};
}
In my case I work with portlets. I think one can easily adapt the binder for servlets:
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyAccessorUtils;
import org.springframework.beans.PropertyValue;
import org.springframework.validation.BindException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.portlet.bind.PortletRequestBindingException;
import org.springframework.web.portlet.bind.PortletRequestParameterPropertyValues;
import javax.portlet.PortletRequest;
import javax.validation.Constraint;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
public class GroupAwarePortletRequestDataBinder extends WebDataBinder
{
public GroupAwarePortletRequestDataBinder(Object target) {
super(target);
}
public GroupAwarePortletRequestDataBinder(Object target, String objectName) {
super(target, objectName);
}
public void bind(PortletRequest request, Class<?> group) throws Exception
{
MutablePropertyValues mpvs = new PortletRequestParameterPropertyValues(request);
MutablePropertyValues targetMpvs = new MutablePropertyValues();
BeanWrapper bw = (BeanWrapper) this.getPropertyAccessor();
for (PropertyValue pv : mpvs.getPropertyValues())
{
if (bw.isReadableProperty(PropertyAccessorUtils.getPropertyName(pv.getName())))
{
PropertyDescriptor pd = bw.getPropertyDescriptor(pv.getName());
for (final Annotation annot : pd.getReadMethod().getAnnotations())
{
Class<?>[] targetGroups = {};
if (BindingGroup.class.isInstance(annot))
{
targetGroups = ((BindingGroup) annot).value();
}
else if (annot.annotationType().getAnnotation(Constraint.class) != null)
{
try
{
final Method groupsMethod = annot.getClass().getMethod("groups");
groupsMethod.setAccessible(true);
try {
targetGroups = (Class<?>[]) AccessController.doPrivileged(new PrivilegedExceptionAction<Object>()
{
@Override
public Object run() throws Exception
{
return groupsMethod.invoke(annot, (Object[]) null);
}
});
}
catch (PrivilegedActionException pae) {
throw pae.getException();
}
}
catch (NoSuchMethodException ignored) {}
catch (InvocationTargetException ignored) {}
catch (IllegalAccessException ignored) {}
}
for (Class<?> targetGroup : targetGroups)
{
if (group.equals(targetGroup))
{
targetMpvs.addPropertyValue(mpvs.getPropertyValue(pv.getName()));
}
}
}
}
}
super.bind(targetMpvs);
}
public void bindAndValidate(PortletRequest request, Class<?> group) throws Exception
{
bind(request, group);
validate(group);
}
public void closeNoCatch() throws PortletRequestBindingException
{
if (getBindingResult().hasErrors()) {
throw new PortletRequestBindingException(
"Errors binding onto object '" + getBindingResult().getObjectName() + "'",
new BindException(getBindingResult()));
}
}
}
Here is an example how your controller method should begin. There are some extra steps required which are normally done by Spring if normal binding mechanism was used.
@ActionMapping
public void onRequest(ActionRequest request, ActionResponse response, ModelMap modelMap) throws Exception
{
Person person = new Person();
GroupAwarePortletRequestDataBinder dataBinder =
new GroupAwarePortletRequestDataBinder(person, "person");
webBindingInitializer.initBinder(dataBinder, new PortletWebRequest(request, response));
initBinder(dataBinder);
BindingResult result = dataBinder.getBindingResult();
modelMap.clear();
modelMap.addAttribute("person", Person);
modelMap.putAll(result.getModel());
}
Some examples for fields of your Person class:
@NotNull(groups = BasicCheck.class)
public String getName() { return name; }
@BindingGroup(BasicCheck.class)
public String phoneNumber() { return phoneNumber; }
@Valid
public Address getAddress() { return address; }
The Address class:
@BindingGroup(BasicCheck.class)
public Integer getZipCode() { return zipCode; }
Writing this answer was lots of work so I hope it helps you.