Adding AnnotatedConstraintBehavior to TextAreas

A while back I blogged about using an annotated constraint behavior to limit input in Wicket TextFields based on domain model annotations (http://blog.armstrongconsulting.com/?p=22). So if we annotate our domain models as follows:

@Length(max = 40)
private String firstName;

that gets automatically be translated into a maxlength=40 attribute on any text field bound to firstName (we use a form visitor on a custom form class to add the behavior to all text fields in the form).

That’s pretty cool, but unfortunately, it only works for text inputs because textarea doesn’t support the maxlength attribute. This really bothered me because its really important to limit the length of textareas since that’s where people try to paste in huge blocks of text and its annoying to only find out the limitation after submitting the form.

So today, I updated the behavior to also handle textareas. There’s a javascript workaround which simulates the maxlength behavior on textareas. I took the javascript from http://cf-bill.blogspot.com/2005/05/unobtrusive-javascript-textarea.html and in the renderHead of the behavior, I add the javascript.

Here’s the code – its in three files: AnnotatedConstraintBehavior.java (the behavior implementation), AnnotatedConstraintBehavior.js (the javascript needed to simulate maxlength on textareas) and ACForm.java (a form which does two extra things: (a) it validates its properties based on annotated constraints (like @NotNull and @Length)) and (b) it adds maxlength to text fields and textareas using the AnnotatedConstraintBehavior).

// AnnotatedConstraintBehavior.java
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import javax.validation.constraints.NotNull;

import org.apache.commons.validator.EmailValidator;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.ResourceReference;
import org.apache.wicket.application.IComponentOnBeforeRenderListener;
import org.apache.wicket.behavior.AbstractBehavior;
import org.apache.wicket.markup.html.IHeaderResponse;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.model.AbstractPropertyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.IPropertyReflectionAwareModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.util.lang.PropertyResolver;
import org.apache.wicket.validation.IValidatable;
import org.apache.wicket.validation.validator.StringValidator;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;

/**
 * Configure a Wicket <code>Component</code> based on Hibernate annotations (@NotNull and @Length(min=x,max=y)).
 * <p>
 * Inspects the <code>Model</code> of a <code>FormComponent</code> and configures the <code>Component</code> according to the declared Hibernate Annotations used on the model object. <br />
 * <strong>NOTE:</strong> This means the <code>Component</code>'s <code>Model</code> <em>must</em> be known when {@link #configure(Component) configuring} a <code>Component</code>.
 * </p>
 *
 * <p>
 * This object can be used as a <code>Behavior</code> to configure a single <code>Component</code>. <br />
 * <strong>NOTE:</strong> this object is <em>stateless</em>, and the same instance can be reused to configure multiple <code>Component</code>s.
 * </p>
 *
 * 

		


 *
 * <p>
 * This object can also be used as a component listener that will automatically configure <em>all</em> <code>FormComponent</code>s based on Hibernate annotations. This ensures that an entire application respects annotations without adding custom
 * <code>Validator</code>s or <code>Behavior</code>s to each <code>FormComponent</code>.
 * </p>
 *
 * 

		


 *
 * @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 {
	static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AnnotatedConstraintBehavior.class);

	@SuppressWarnings("unchecked")
	private static Map configs = new HashMap() {
		{
			put(NotNull.class, new AnnotationConfig() {
				public void onAnnotatedComponent(Annotation annotation, FormComponent component) {
					component.setRequired(true);
				}
			});
			put(Length.class, new AnnotationConfig() {
				public void onAnnotatedComponent(Annotation annotation, FormComponent component) {
					int max = ((Length) annotation).max();
					log.debug("adding maxlength=" + max + " attribute to " + component.getMarkupId());
					component.add(new AttributeModifier("maxlength", true, new Model(Integer.toString(max))));
					component.add(StringValidator.maximumLength(max));
				}
			});

			put(Email.class, new AnnotationConfig() {
				public void onAnnotatedComponent(Annotation annotation, FormComponent component) {
					component.add(new StringValidator() {
						@Override
						protected void onValidate(IValidatable<string> validatable) {
							if (!EmailValidator.getInstance().isValid(validatable.getValue())) {
								error(validatable);
							}

						}
					});
				}
			});

		}
	};

	@Override
	public void renderHead(IHeaderResponse iHeaderResponse) {
		super.renderHead(iHeaderResponse);
		iHeaderResponse.renderJavascriptReference(new ResourceReference(AnnotatedConstraintBehavior.class, "AnnotatedConstraintBehavior.js"));
	}

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

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

	void configure(Component component) {
		if (!isApplicableFor(component)) {
			return;
		}
		FormComponent<?> formComponent = (FormComponent<?>) component;
		for (Annotation annotation : getAnnotations(component.getDefaultModel())) {
			Class<? extends Annotation> annotationType = annotation.annotationType();
			AnnotationConfig config = (AnnotationConfig) configs.get(annotationType);
			if (null != config) {
				config.onAnnotatedComponent(annotation, formComponent);
			}
		}
	}

	private Collection<annotation> getAnnotations(IModel<?> model) {

		try {
			// Only if a setter method is available we'll search for the
			// related field and find its Annotations.
			Method setter = ((IPropertyReflectionAwareModel) model).getPropertySetter();

			String name = setter.getName();

			if (name.startsWith("set") &amp;&amp; name.length() > 3) {
				name = name.substring(3, 4).toLowerCase() + name.substring(4);
			} else {
				return Collections.emptyList();
			}

			Object target = ((AbstractPropertyModel<?>) model).getTarget();
			Field field = PropertyResolver.getPropertyField(name, target);
			if (field == null) {
				return Collections.emptyList();
			}

			return Arrays.asList(field.getAnnotations());
		} catch (Exception ignore) {
			return Collections.emptyList();
		}
	}

	private boolean isApplicableFor(Component component) {
		if (!(component instanceof FormComponent<?>)) {
			return false;
		}
		IModel<?> model = component.getDefaultModel();
		if (model == null || !IPropertyReflectionAwareModel.class.isAssignableFrom(model.getClass())) {
			return false;
		}

		return true;
	}

	/**
	 * simple interface to abstract performing work for a specific annotation.
	 */
	private static interface AnnotationConfig extends Serializable {
		void onAnnotatedComponent(Annotation annotation, FormComponent<?> component);
	}
}
// AnnotatedConstraintBehavior.js
// javascript to simulate maxlength behavior on textareas
// source: http://cf-bill.blogspot.com/2005/05/unobtrusive-javascript-textarea.html

<script type="text/javascript">

 function textAreasInit(){
  var objs = document.getElementsByTagName("textarea");
  var oi = 0; //oi is object index
  var thisObj;

  for (oi=0;oi<objs.length;oi++) {
   thisObj = objs&#91;oi&#93;;
   // note that maxlength is case sensitve
   if (thisObj.getAttribute('maxlength')){
    thisObj.onkeyup = forceMaxLength;
   }
   thisObj.onchange = saveEntryValue;
  }
 }

 function forceMaxLength(){
  var maxLength = parseInt(this.getAttribute('maxlength'));
  if(this.value.length > maxlength){
   this.value = this.value.substring(0,maxlength);
  }
 }

function addEvent(elm, evType, fn, useCapture)
// addEvent and removeEvent
// cross-browser event handling for IE5+,  NS6 and Mozilla
// By Scott Andrew
{
  if (elm.addEventListener){
 elm.addEventListener(evType, fn, useCapture);
 return true;
  } else if (elm.attachEvent){
 var r = elm.attachEvent("on"+evType, fn);
 return r;
  } else {
 alert("Handler could not be removed");
  }
}

addEvent(window, "load", textAreasInit);
</script>
// ACForm.java
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

import org.apache.wicket.Component;
import org.apache.wicket.Component.IVisitor;
import org.apache.wicket.behavior.AbstractBehavior;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.IModel;
import org.wicketstuff.jsr303.PropertyValidation;

public class ACForm<t> extends Form<t> {
	private static final long serialVersionUID = 1L;

	static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Placeholder.class);

	public ACForm(String id, IModel<t> model) {
		super(id, model);
		add(new PropertyValidation());
	}

	public ACForm(String id) {
		super(id);
	}

	@Override
	protected void onBeforeRender() {
		super.onBeforeRender();
		visitChildren(TextField.class, new AddBehaviorVisitor(new AnnotatedConstraintBehavior()));

		// note: textarea doesn't actually support the maxlength attribute, but we add it anyway and enforce it with javascript (the AnnotatedConstraintBehavior adds it)
		visitChildren(TextArea.class, new AddBehaviorVisitor(new AnnotatedConstraintBehavior()));
	}
}

class AddBehaviorVisitor implements IVisitor<component>, Serializable {
	private static final long serialVersionUID = 1L;
	private final AbstractBehavior behavior;
	Set<component> visited = new HashSet<component>();

	public AddBehaviorVisitor(AbstractBehavior behavior) {
		this.behavior = behavior;
	}

	@Override
	public Object component(Component component) {
		if (!visited.contains(component)) {
			visited.add(component);
			component.add(behavior);
		}
		return IVisitor.CONTINUE_TRAVERSAL;
	}
}

Leave a Reply

Your email address will not be published.