A little less than a year ago, I released Regula, an annotation-based form-validation written in Javascript. The source and documentation are available on GitHub. I started working on the integration on and off throughout most of last year. At the end of the year, I had a pretty good integration going, where you could annotate fields with Hibernate Validator annotations, and the corresponding Regula validation-code would be generated on the client side. Of course, I wasn’t done yet because what I had was simply a demo project and I had to figure out a good way to distribute the whole thing; I was able to finish up the packaging and distribution today. With minimal setup, you should be able to get started with Regula and Spring. You don’t need to go through this post to figure out how to use the integration. This post is mostly about how I accomplished the integration (I don’t go into all the details; just the important bits). As far as actually using it, I will make a blog post about it later.
The source for the integration is also hosted on GitHub. My approach towards translating validation constraints from the server-side to the client-side was two-fold: gather validation constraints from the object and represent it in a canonical form. Using the canonical form, generate Javascript code that uses Regula for validation. To do this, I created a service that examines a domain object and gathers all information regarding its properties and validation constraints. The service returns this information in a canonical form, that I then inserted into the model. On the client-side, I had a tag that used the canonical form and outputted Javascript that uses the Regula framework. Initially, I was calling the service explicitly from an action in the controller. Later, in an effort to make the integration less-invasive and more seamless, I used an aspect-oriented approach with interceptors. In fact, that’s where I’d like to start.
I created an annotation called @ValidateClientSide that marks a domain object as requiring client-side validation. Then, I created an interceptor called ClientSideValidationInterceptor that will call the validation service and gather validation information:
ValidateClientSide.java:
package net.vivin.regula.validation;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited public @interface ValidateClientSide { } [/sourcecode]
The code for the interceptor is pretty simple. It inspects the model for objects that are annotated with ValidateClientSide, and then uses the service to gather validation information:
ClientSideValidationInterceptor.java:
package net.vivin.regula.validation;
import net.vivin.regula.validation.service.ClassConstraintInformation; import net.vivin.regula.validation.service.ValidationConstraintsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.annotation.Annotation; import java.util.Map; import java.util.Set;
public class ClientSideValidationInterceptor extends HandlerInterceptorAdapter {
@Autowired ValidationConstraintsService validationConstraintsService;
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if(modelAndView != null) {
Map modelMap = modelAndView.getModel();
if(modelMap != null) {
Set> modelMapEntrySet = modelMap.entrySet();
Map.Entry[] modelMapEntryArray = modelMapEntrySet.toArray(new Map.Entry[modelMapEntrySet.size()]);
boolean found = false; int i = 0;
while(i < modelMapEntrySet.size() && !found) {
Map.Entry entry = modelMapEntryArray[i];
Object value = entry.getValue();
if(hasValidateClientSideAnnotation(value)) {
found = true;
ClassConstraintInformation classConstraintsInformation = validationConstraintsService.getValidationConstraints(value.getClass());
modelAndView.getModelMap().addAttribute("propertyToConstraintInstancesMap", classConstraintsInformation.getPropertyToConstraintInstancesMap());
modelAndView.getModelMap().addAttribute("compoundConstraintDefinitionSet", classConstraintsInformation.getCompoundConstraintDefinitionSet());
}
i++;
}
}
}
}
private boolean hasValidateClientSideAnnotation(Object object) {
boolean found = false;
int i = 0;
if (object != null) {
Annotation[] annotations = object.getClass().getDeclaredAnnotations();
while(i < annotations.length && !found) {
Annotation annotation = annotations[i];
found = annotation.annotationType().equals(ValidateClientSide.class);
i++;
}
}
return found;
}
}
[/sourcecode]
Now the hard part. Getting validation information. This part of the integration involves a lot of reflection (and some recursion, especially in the case of compound constraints and @Valid). The basic strategy is as follows:
-
For each constraint property in the class: 2. If the property is cascaded, get the class of the property and call this method again (recursion). 3. If the property is not cascaded, for each constraint attached to the property: 4. If the constraint is a composing constraint recursively identify and record each composing constraint within this constraint. 5. if the constraint is not a composing constraint, simply record it.
The actual code that does all this is slightly more verbose though:
ValidationConstraintsService.java:
package net.vivin.regula.validation.service;
import net.vivin.regula.validation.constraint.ConstraintDefinition; import net.vivin.regula.validation.constraint.ConstraintInstance; import net.vivin.regula.validation.constraint.ConstraintParameter;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.type.ClassMetadata; import org.springframework.stereotype.Service;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import javax.validation.groups.Default; import javax.validation.metadata.BeanDescriptor; import javax.validation.metadata.ConstraintDescriptor; import javax.validation.metadata.PropertyDescriptor;
import java.lang.annotation.Annotation; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern;
@Service public class ValidationConstraintsService {
@Autowired private LocalValidatorFactoryBean validator;
private Map>> classToPropertyToConstraintInstancesMap = new LinkedHashMap>>(); private Map> classToCompoundConstraintDefinitionMap = new HashMap>();
private String getFriendlyNameForProperty(String propertyName) {
String[] parts = StringUtils.splitByCharacterTypeCamelCase(propertyName);
for(int i = 0; i < parts.length ; i++) {
parts[i] = parts[i].toLowerCase().replaceAll("\\.", " ");
}
parts[0] = StringUtils.capitalize(parts[0]);
return StringUtils.join(parts, " ").replaceAll("\\s+", " ");
}
/* The following method uses a recursive algorithm that inspects a class and identifies any annotations (and their parameters) that are attached to a member field. If any field is annotated with a @Valid annotation, the method figures out the type of that field (which should be a complex type) and performs the same inspection on the class that represents that type.
The clazz parameter is the current class we're working on i.e., the class who's validation constraints we want The second parameter holds the prefix. This is used to build a path. So that can get something like "object.fieldName" The third parameter (propertyConstraintInstancesMap) is a map that is keyed by property. The value is a set of ConstraintInstance objects. The last parameter maintains a set of compound-constraint definitions that we encounter in this class. */
private void _getConstraints(Class clazz, String prefix, Map> propertyToConstraintInstancesMap, Map compoundConstraintDefinitionMap) {
BeanDescriptor classDescriptor = validator.getConstraintsForClass(clazz);
for(PropertyDescriptor propertyDescriptor : classDescriptor.getConstrainedProperties()) {
Set constraintInstances = new HashSet();
String propertyName;
if(StringUtils.isEmpty(prefix)) {
propertyName = propertyDescriptor.getPropertyName();
}
else {
propertyName = prefix + "." + propertyDescriptor.getPropertyName();
}
String friendlyName = getFriendlyNameForProperty(propertyName);
if(propertyDescriptor.isCascaded()) {
String newPrefix;
if(StringUtils.isEmpty(prefix)) {
newPrefix = StringUtils.uncapitalize(propertyDescriptor.getPropertyName());
}
else {
newPrefix = prefix + "." + propertyDescriptor.getPropertyName();
}
_getConstraints(propertyDescriptor.getElementClass(), newPrefix, propertyToConstraintInstancesMap, compoundConstraintDefinitionMap); }
else {
for(ConstraintDescriptor? extends Annotation constraintDescriptor : propertyDescriptor.getConstraintDescriptors()) {
ConstraintInstance validationConstraintInstance = createConstraintFromDescriptor(constraintDescriptor);
if(constraintDescriptor.getComposingConstraints().size() > 0) {
handleComposingConstraints(validationConstraintInstance, constraintDescriptor, compoundConstraintDefinitionMap);
}
validationConstraintInstance.addParameter(new ConstraintParameter("label", friendlyName, "String"));
constraintInstances.add(validationConstraintInstance);
}
}
if(constraintInstances.size() > 0) {
propertyToConstraintInstancesMap.put(propertyName, constraintInstances);
}
}
}
/* this method uses an algorithm similar to the one above to get class-level constraints (i.e., validation constraints attached to a bean */
private void getClassLevelConstraints(Class clazz, Map> propertyToConstraintInstancesMap, Map compoundConstraintDefinitionMap) {
String friendlyName = StringUtils.uncapitalize(clazz.getSimpleName());
Set constraintInstances = new HashSet();
BeanDescriptor classDescriptor = validator.getConstraintsForClass(clazz); Set> classLevelConstraintDescriptors = classDescriptor.getConstraintDescriptors();
for(ConstraintDescriptor? constraintDescriptor : classLevelConstraintDescriptors) {
ConstraintInstance validationConstraintInstance = createConstraintFromDescriptor(constraintDescriptor);
if(constraintDescriptor.getComposingConstraints().size() > 0) {
handleComposingConstraints(validationConstraintInstance, constraintDescriptor, compoundConstraintDefinitionMap);
}
validationConstraintInstance.addParameter(new ConstraintParameter("label", friendlyName, "String"));
validationConstraintInstance.setClassLevelConstraint(true);
constraintInstances.add(validationConstraintInstance); }
if(constraintInstances.size() > 0) {
propertyToConstraintInstancesMap.put(friendlyName, constraintInstances);
}
}
/* this method creates a constraint instance from a ConstraintDescriptor object */
private ConstraintInstance createConstraintFromDescriptor(ConstraintDescriptor? extends Annotation constraintDescriptor) {
Annotation annotation = constraintDescriptor.getAnnotation(); ConstraintInstance validationConstraintInstance = new ConstraintInstance(annotation.annotationType().getSimpleName()); Map attributes = new LinkedHashMap(constraintDescriptor.getAttributes());
for(Map.Entry entry : attributes.entrySet()) {
if(!"payload".equals(entry.getKey())) {
validationConstraintInstance.addParameter(createConstraintParameter(annotation, entry));
}
}
return validationConstraintInstance; }
/* This method recursively figures out the composing constraints of a constraint, and adds it to the compound-constraint definition set */
private void handleComposingConstraints(ConstraintInstance parentConstraintInstance, ConstraintDescriptor? extends Annotation parentDescriptor, Map compoundConstraintDefinitionMap) {
ConstraintDefinition parentConstraintDefinition = new ConstraintDefinition(parentConstraintInstance);
/* definitely not the way I would like to do it, but there seems to be no way other way to identify hibernate or java
validation-constraints that are compound-constraints themselves
*/
if(parentDescriptor.getAnnotation().annotationType().getName().indexOf("javax.validation") < 0 &&
parentDescriptor.getAnnotation().annotationType().getName().indexOf("org.hibernate") < 0) {
parentConstraintDefinition.setReportAsSingleViolation(parentDescriptor.isReportAsSingleViolation());
for(ConstraintDescriptor? extends Annotation composingDescriptor : parentDescriptor.getComposingConstraints()) {
ConstraintInstance composingConstraintInstance = createConstraintFromDescriptor(composingDescriptor);
if(composingDescriptor.getComposingConstraints().size() > 0) {
handleComposingConstraints(composingConstraintInstance, composingDescriptor, compoundConstraintDefinitionMap);
}
parentConstraintDefinition.addComposingConstraint(composingConstraintInstance); }
if(compoundConstraintDefinitionMap.get(parentConstraintDefinition.getName()) != null) {
ConstraintDefinition existing = compoundConstraintDefinitionMap.get(parentConstraintDefinition.getName());
for(ConstraintInstance composing : existing.getComposingConstraints()) {
parentConstraintDefinition.addComposingConstraint(composing);
}
}
compoundConstraintDefinitionMap.put(parentConstraintDefinition.getName(), parentConstraintDefinition);
}
}
//There is no straightforward way to translate some parameter values into a string. This is especially true in the case of the @Pattern annotation where
//the flags parameter is of type Pattern.Flag[] and the value is a array of Pattern.Flag[] objects (enums actually). So here, we inspect the parameters
//and their values to perform specific translations if needed
private ConstraintParameter createConstraintParameter(Annotation annotation, Map.Entry entry) {
String parameterName = entry.getKey();
Object parameterValue = entry.getValue();
String parameterType = parameterValue.getClass().getSimpleName();
String annotationName = annotation.annotationType().getSimpleName();
if("Pattern".equals(annotationName) && "flags".equals(parameterName)) {
String flags = "";
for(javax.validation.constraints.Pattern.Flag flag : (javax.validation.constraints.Pattern.Flag[]) parameterValue) {
//The only flags that make sense in Javascript are i, m, and g. i and m map to CASE_INSENSITIVE
//and MULTILINE. g doesn't map to a flag enum in the Java world, and the remaining Java enums
//don't map to anything in the Javascript world
switch(flag) {
case CASE_INSENSITIVE:
flags += "i";
break;
case MULTILINE:
flags += "m";
break;
}
}
parameterValue = flags; parameterType = "String";
}
else if("message".equals(parameterName)) {
if(parameterValue.toString().startsWith("{") && parameterValue.toString().endsWith("}")) {
parameterValue = parameterValue.toString().replace("{", "").replace("}", "");
}
}
else if("groups".equals(parameterName)) {
String groups = "";
for(Class clazz : (Class[]) parameterValue) {
groups += clazz.getSimpleName() + ",";
}
Pattern pattern = Pattern.compile(",$", Pattern.DOTALL);
Matcher matcher = pattern.matcher(groups);
groups = matcher.replaceFirst("");
if(StringUtils.isEmpty(groups)) {
groups = Default.class.getSimpleName();
}
parameterValue = "[" + groups + "]";
parameterType = "Array";
}
return new ConstraintParameter(parameterName, parameterValue.toString(), parameterType); }
public ClassConstraintInformation getValidationConstraints(Class clazz) {
Map> propertyToConstraintInstancesMap = classToPropertyToConstraintInstancesMap.get(clazz);
Map compoundConstraintDefinitionMap = classToCompoundConstraintDefinitionMap.get(clazz);
if(propertyToConstraintInstancesMap == null) {
propertyToConstraintInstancesMap = new LinkedHashMap>();
compoundConstraintDefinitionMap = new LinkedHashMap();
_getConstraints(clazz, "", propertyToConstraintInstancesMap, compoundConstraintDefinitionMap); getClassLevelConstraints(clazz, propertyToConstraintInstancesMap, compoundConstraintDefinitionMap);
classToPropertyToConstraintInstancesMap.put(clazz, propertyToConstraintInstancesMap);
classToCompoundConstraintDefinitionMap.put(clazz, compoundConstraintDefinitionMap);
}
if(compoundConstraintDefinitionMap == null) {
compoundConstraintDefinitionMap = new LinkedHashMap();
}
return new ClassConstraintInformation(
new HashMap>(propertyToConstraintInstancesMap),
new HashSet(compoundConstraintDefinitionMap.values())
);
}
}
[/sourcecode]
So the service above returns our information in a form that we can use. We’re essentially going to translate this information into the Regula framework. To do this, we need to write a tag. The tag goes through the gathered information and figures out the property name, the constraints attached to the property, and the parameters to the constraints. The tag also figures out the composing constraints for compound constraints and translates that to Regula compound-constraints:
generate-validation.tag:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <%@ taglib prefix="string" uri="http://regula-spring" %>
[/sourcecode]
This tag also takes care of binding the submit handler (using jQuery), which runs the validation on submit and displays error messages if any. If you need to do anything more complicated, you can override the submit handler.
That’s pretty much it. In another blog post, I’ll talk about actually using the integration.
Sign in to leave a comment.
Loading…