Annotated constraints

We were wondering how to use Hibernate annotations to automatically create constraints (HTML “maxlength”) and validators (required fields, string length validation). After searching around, I found a wicket behavior for this in wicket-stuff from Ryan Sonnek. It was in wicket-stuff 1.3 and apparently had been removed from 1.4. Anyway, I patched it up a bit to make it work in wicket 1.4 and here it is (I also took the liberty of renaming it to AnnotatedConstraintBehavior in case we wanted to use it for annotations other than hibernate):

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.application.IComponentOnBeforeRenderListener;
import org.apache.wicket.behavior.AbstractBehavior;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.IPropertyReflectionAwareModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.validation.validator.StringValidator;
import org.hibernate.validator.Length;
import org.hibernate.validator.NotNull;

/**
 * Configure a Wicket Component based on Hibernate annotations (@NotNull and @Length(min=x,max=y)).
 *
 * Inspects the Model of a FormComponent and configures the Component according to the declared Hibernate Annotations used on the model object.
 * NOTE: This means the Component's Model mustbe known when configuring a Component.
 *
 * This object can be used as a Behavior to configure a single Component.
 * NOTE: this object is stateless, and the same instance can be reused to configure multiple Components.
 *
 * public class MyWebPage extends WebPage {
 * 	public MyWebPage() {
 *     TextField name = new TextField("id", new PropertyModel(user, "name");
 *     name.addBehavior(new AnnotatedConstraintBehavior());
 *     add(name);
 *   }
 * }
 *
 * This object can also be used as a component listener that will automatically configure all FormComponents based on Hibernate annotations. This ensures that an entire application respects annotations without adding custom Validators or Behaviors to each FormComponent.
 *
 * public class MyApplication extends WebApplication {
 * 	public void init() {
 * 		addPreComponentOnBeforeRenderListener(new AnnotatedConstraintBehavior());
 * 	}
 * }
 *
 * @see http://jroller.com/page/wireframe/?anchor= hibernateannotationcomponentconfigurator
 * @see http ://jroller.com/page/wireframe/?anchor=hibernate_annotations_and_wicket
 */
@SuppressWarnings("serial")
public class AnnotatedConstraintBehavior extends AbstractBehavior implements IComponentOnBeforeRenderListener {
	@SuppressWarnings("unchecked")
	private static Map configs = new HashMap() {
		{
			put(NotNull.class, new HibernateAnnotationConfig() {
				public void onAnnotatedComponent(Annotation annotation, FormComponent component) {
					component.setRequired(true);
				}
			});
			put(Length.class, new HibernateAnnotationConfig() {
				public void onAnnotatedComponent(Annotation annotation, FormComponent component) {
					int max = ((Length) annotation).max();
					component.add(new AttributeModifier("maxlength", true, new Model(Integer.toString(max))));
					component.add(StringValidator.maximumLength(max));
				}
			});
		}
	};

	@Override
	public final void bind(Component component) {
		super.bind(component);

		configure(component);
	}

	public final void onBeforeRender(Component component) {
		if (!component.hasBeenRendered()) {
			configure(component);
		}
	}

	void configure(Component component) {
		if (!isApplicableFor(component)) {
			return;
		}
		FormComponent formComponent = (FormComponent) component;
		IPropertyReflectionAwareModel propertyModel = (IPropertyReflectionAwareModel) component.getDefaultModel();
		for (Annotation annotation : getAnnotations(propertyModel)) {
			Class annotationType = annotation.annotationType();
			HibernateAnnotationConfig config = (HibernateAnnotationConfig) configs.get(annotationType);
			if (null != config) {
				config.onAnnotatedComponent(annotation, formComponent);
			}
		}
	}

	private Collection getAnnotations(IPropertyReflectionAwareModel propertyModel) {
		Field field = propertyModel.getPropertyField();
		if (field == null) {
			//Log.warn("Unable to find annotations for model: " + propertyModel);
			return Collections.emptyList();
		}
		return Arrays.asList(field.getAnnotations());
	}

	private boolean isApplicableFor(Component component) {
		if (!(component instanceof FormComponent)) {
			return false;
		}
		IModel model = component.getDefaultModel();
		if (null == model || !IPropertyReflectionAwareModel.class.isAssignableFrom(model.getClass())) {
			//Log.warn("No valid model is available for configuring Component: " + component);
			return false;
		}

		return true;
	}

	/**
	 * simple interface to abstract performing work for a specific annotation.
	 */
	private static interface HibernateAnnotationConfig {
		void onAnnotatedComponent(Annotation annotation, FormComponent component);
	}
}

To use it, you’ll need to have @Length(min=x,max=y) and/or @NotNull hibernate annotations on your objects. The behavior will find these in component models and react accordingly, adding the maxlength attributes to the markup, string validator and setting required.