|
1.Summary
The method of using Hibernate Validator for validation of input data in JSF(TM) is described. Validation of input data partially belongs to the restriction of domain object. For example, validation to be four-digit has two part. One is to be digit. This is a type check. Two is four digits. This may belong to domain object. I want to take the restriction which belongs to domain object into the domain object. This is a reason to have tried to use Hibernate Validator.
However, to use Hibernate Validator with JSF, it is not ready. Rodrigo Urubatan 's Blog [1] shows an excellent solution that implemets validator as an Intercepter( ValidationInterceptor ). But, I feel two inconveniences.
1) Validation is executed whenever Domain Object is updated. Only validation phase in JSF wants to execute validation.
2) The method of searching component to display the validation error is weak. Looking for the input tag scanning component tree has a difficulty that there may be the same componet name.
I did the following device.
1) Execute validation by Intercepter detecting the validation phase of JSF.
2) Make a general Validator( DomainValidator ) that did not depend for the processing of Validation yet according to the JSF standard. DomainValidator do not excute validation, but only lead input data to ValidationInterceptor. ValidationInterceptor returns DomainValidator the validation error as an exception or messages via ThreadLocal or RequestMap of FacesContext. Because DomainValidator obeys JSF standard, there is no problem to present error messages from DomainValidator.
2. DomainValidator
Validator of JSF doesn't have the concept of Object that is updating. But, Object can get from the "value" attribute of input tag. I will show you by example.
<h:inputText size="2" value="#{select.number}" required="true"> <f:validator validatorId="DomainValidator"/> </h:inputText>
#{select.number} expressed by Expression Language( EL ) means that the method setNumber of the object named "select" is invoked at input. Evaluation of #{select} gives us the object named "select".
The program is shown in list 1. The code became complex because it deals with wrapper classes of Java(TM).
3. ValidationInterceptor
ValidationInterceptor is shown in list 2. The difference with Rodrigo Urubatan's Blog[1] is not using getInvalidValues(), but getPotentialInvalidValues(). This is because we do not update the value of object in validation phase. The purpose of ValidationHelper.performValidation() is to execute validation in validation phase. In other phase, ValidationInterceptor do not validate, but only invoke the method of the domain object ( pass through ). Original annotation jp.co.exa_corp.validator.Validation is used to specify Pointcut surely and to display the property name in Japanese (list 3). SoftHashMap[2] that Dr. Heinz Kabutz had made was changed to Java 5 generic ( list. 6 ) and is used to cache the instance of ClassValidator.
4. ValidationPhaseListener and ValidationHelper
These are classes (list 4, 5) to inform ValidationInterceptor in validation phase. In a test without the environment of JSF, validation is always executed.
5.Setting
1) faces-config.xml
<validator> <validator-id>DomainValidator</validator-id> <validator-class>jp.co.exa_corp.validator.hibernate.DomainValidator</validator-class> </validator>
<lifecycle> <phase-listener>jp.co.exa_corp.validator.hibernate.interceptor.ValidationPhaseListener</phase-listener> </lifecycle>2) META-INF/aop.xml
When you use Load Time Weaving by AspectJ, it is necessary to set aop.xml. The setting is shown in the reference though this depends on the application.
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd"> <aspectj> <!-- If you neeed weaving informations <weaver options="-verbose -debug -showWeaveInfo"> --> <!-- only weave classes in our application-specific packages --> <weaver> <include within="jp.co.exa_corp.example.ordering.domain.*"/> <exclude within="org.*"/> </weaver>
<!-- weave in just this aspect --> <aspects> <aspect name="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"/> <aspect name="jp.co.exa_corp.validator.hibernate.interceptor.ValidationInterceptor"/> </aspects> </aspectj>
6. Attention
In DomainValidator, it is necessary to program it so that the input may change Domain Object. If input data is substituted for a variable, validation cannot be done. This becomes remarkable if you use a display model . In this case, you must map the display model to domain model. I will explain this by example.
public class CartItem { private Item item; public CartItem( Item item ) { this.item = item; } public getProductName() { return item.getProduct().getName(); } public int getNumber() { return item.getNumber(); } public void setNumber( int number ) { item.setNumber( number ); } }
Here, CartItem is a display model, and Item is a domain model. There assumed to be a restriction that limits the range of the numerical value of item.number. In setNumber(), delegation is used to setting number to item( tightly coupled ). When this is written as follows, DomainValidator doesn't work.
public void setNumber( int number ) { this.number = number; }
References
[1] http://weblogs.java.net/blog/urubatan/archive/2006/09/aspectj_hiberna.html [2] http://archive.devx.com/java/free/articles/Kabutz01/Kabutz01-1.asp |
|
list1. DomainValidator
package jp.co.exa_corp.validator.hibernate;
import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException;
import javax.faces.FacesException; import javax.faces.context.FacesContext; import javax.faces.component.UIComponent; import javax.faces.application.FacesMessage; import javax.faces.validator.Validator; import javax.faces.validator.ValidatorException;
import jp.co.exa_corp.validator.ValidationException;
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * * DomainValidator : Path through to ValdatotionInterceptor * * @author Masato Tuschiya 2008/7/15 */
public class DomainValidator implements Validator { private static Log log = LogFactory.getLog( DomainValidator.class ); public void validate( FacesContext context, UIComponent component, Object value ) throws ValidatorException { String el = component.getValueExpression("value").getExpressionString();
// Get target object of expression language in the input tag // ex. <h:inputText size="2" value="#{select.number}" required="true"> // target = select Object target = context.getApplication().getExpressionFactory(). createValueExpression( context.getELContext(), getTargetObjectExpression( el ), Object.class ). getValue( context.getELContext());
log.trace("validate: ("+target+")."+getTargetMethodName(el)+"("+ value.getClass().getName()+" "+value+")"); Method method = null; String methodName = getTargetMethodName( el );
try { // Invoke the target method // ex. <h:inputText size="2" value="#{select.number}" required="true"> // invoke select.setNumber( value ); method = target.getClass().getMethod( methodName, new Class[]{ value.getClass()});
} catch( NoSuchMethodException e ) { Class type = Void.TYPE; if( value instanceof Short ) { type = Short.TYPE; } else if( value instanceof Long ) { type = Long.TYPE; } else if( value instanceof Integer ) { type = Integer.TYPE; } else if( value instanceof Double ) { type = Double.TYPE; } else if( value instanceof Float ) { type = Float.TYPE; } else if( value instanceof Byte ) { type = Byte.TYPE; } else if( value instanceof Character ) { type = Character.TYPE; } else if( value instanceof Boolean ) { type = Boolean.TYPE; } else { throw new NoSuchMethodError("method: "+ methodName +"("+ value.getClass().getName() +") not found"); } try { method = target.getClass().getMethod( methodName, new Class[]{ type }); } catch( NoSuchMethodException n ) { throw new NoSuchMethodError("method: "+ methodName +"("+ value.getClass().getName() +") not found"); } } try { method.invoke( target, new Object[]{ value }); } catch( InvocationTargetException t ) { throw new ValidatorException( new FacesMessage( t.getTargetException().getMessage())); } catch( Exception e ) { throw new IllegalAccessError( e.getMessage()); } } private static String getTargetMethodExpression( String s ) { // #{a.b} ==> #{a.setB} s = s.substring( 2, s.length() - 1 ); int n = s.lastIndexOf("."); // ToDo for n < 0 return "#{"+ s.substring( 0, n ) +".set"+ capitalize( s.substring( n+1, s.length())) +"}"; } private static String getTargetObjectExpression( String s ) { // #{a.b.c} ==> #{a.b} s = s.substring( 2, s.length() - 1 ); int n = s.lastIndexOf("."); return "#{" + ( n < 0 ? s : s.substring( 0, n )) + "}"; } private static String getTargetMethodName( String s ) { // #{a.b} ==> setB s = s.substring( 2, s.length() - 1 ); int n = s.lastIndexOf("."); // ToDo for n < 0 return "set"+ capitalize( s.substring( n+1, s.length())); } private String getTargetPropertyName( final String s ) { // #{a.b} ==> b int n = s.lastIndexOf("."); // ToDo for n < 0 return s.substring( n+1, s.length() - 1 ); } private static String capitalize( final String s ) { // abc ==> Abc return s.substring(0,1).toUpperCase() + s.substring(1,s.length()); } }
list2. ValidationInterceptor
package jp.co.exa_corp.validator.hibernate.interceptor;
import java.util.Map; import java.lang.reflect.Method; import java.lang.ref.WeakReference; import java.lang.annotation.Annotation;
import jp.co.exa_corp.validator.Validation; // exa annotation import jp.co.exa_corp.validator.ValidationException;
import org.aspectj.lang.Signature; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut;
import org.hibernate.validator.ClassValidator; import org.hibernate.validator.InvalidValue;
import jp.co.exa_corp.validator.Validation; // exa annotation import jp.co.exa_corp.validator.ValidationException;
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * <BR>ValidationInterceptor : Validator Using Hibernate Validator</BR> * * @author Masato Tuschiya 2008/8/07 * @see http://weblogs.java.net/blog/urubatan/archive/2006/09/aspectj_hiberna.html * */ @Aspect public class ValidationInterceptor implements java.io.Serializable { private static Log log = LogFactory.getLog( ValidationInterceptor.class );
// Our use own annotation:jp.co.exa_corp.validator.Validation to detect pointcut @Pointcut("execution( @jp.co.exa_corp.validator.Validation public * *.set*(*))") // only one argument public void needValidation(){}
@Around("needValidation()") @SuppressWarnings("unchecked") public Object validate( final ProceedingJoinPoint thisJoinPoint ) throws Throwable { final Object[] args = thisJoinPoint.getArgs(); final String method = thisJoinPoint.getSignature().getName(); log.debug("validate:"+ thisJoinPoint.getSignature() +" "+ args[0].getClass().getName() +"="+ args[0]); if( ValidationHelper.performValidation() && args != null && args.length == 1 && isSetter( method )) { final Class clazz = thisJoinPoint.getTarget().getClass(); Validation validation = (Validation)((MethodSignature)thisJoinPoint.getSignature()). getMethod().getAnnotation( Validation.class ); if( validation == null ) { throw new NoSuchMethodError("method: "+ thisJoinPoint.getSignature() +" has not @Validation"); }
final ClassValidator validator = getClassValidator( clazz ); // We don't update value but only validate. final InvalidValue[] invalidmessages=validator.getPotentialInvalidValues(getPropertyName( method ),args[0]); if ((invalidmessages != null) && (invalidmessages.length > 0)) { StringBuilder buff = new StringBuilder(); for( InvalidValue iv : invalidmessages ) { buff.append( "¥"" ); // jp.co.exa_corp.validator.Validation.expression = attribute name buff.append( validation.expression()); buff.append( "¥"" ); buff.append( iv.getMessage()); buff.append( "¥n" ); } buff.setLength( buff.length() - 1 ); log.debug("validate:" + buff.toString()); throw new ValidationException( buff.toString()); } } return thisJoinPoint.proceed( args ); } private boolean isSetter( String method ) { return method.startsWith( "set" ); } private String getPropertyName( final String method ) { return method.substring( 3, 4 ).toLowerCase() + method.substring( 4 ); } // Cache for Hibernate Validator private static final Map< Class, ClassValidator> validators; static { validators = new SoftHashMap< Class, ClassValidator>(); } @SuppressWarnings("unchecked") private static ClassValidator getClassValidator( Class clazz ) { ClassValidator validator = null; synchronized( validators ) { validator = validators.get( clazz ); if( validator == null ) { validator = new ClassValidator( clazz ); validators.put( clazz, validator ); } } return validator; } }
list3. Validation
package jp.co.exa_corp.validator;import java.lang.annotation.Target; import java.lang.annotation.Retention; import java.lang.annotation.Inherited; import java.lang.annotation.Documented; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target( METHOD ) @Retention( RUNTIME ) @Documented @Inherited public @interface Validation { public String expression(); }
list4. ValidationPhaseListener
package jp.co.exa_corp.validator.hibernate.interceptor;
import javax.faces.event.PhaseId; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseListener;
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * * ValidationPhaseListener * * @author Masato Tuschiya 2008/7/15 */ public class ValidationPhaseListener implements PhaseListener { private static Log log = LogFactory.getLog( ValidationPhaseListener.class ); public void beforePhase(javax.faces.event.PhaseEvent event) { log.trace("beforePhase:"+ event.getPhaseId()); ValidationHelper.setPhaseId( event ); } public void afterPhase(javax.faces.event.PhaseEvent event) { log.trace("afterPhase:"+ event.getPhaseId()); } public PhaseId getPhaseId() { return PhaseId.ANY_PHASE; } }
list5. ValidationHelper
package jp.co.exa_corp.validator.hibernate.interceptor;
import java.util.List; import java.util.ArrayList; import javax.faces.context.FacesContext; import javax.faces.event.PhaseId; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseListener;
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * * ValidationHelper : Helper for ValidationInterceptor * * @author Masato Tuschiya 2008/8/07 */ public class ValidationHelper { private static final String PHASE_ID_KEY = "ValidatorHelper_PhaseId_Key"; private static Log log = LogFactory.getLog( ValidationHelper.class ); public static synchronized void setPhaseId( PhaseEvent event ) { event.getFacesContext().getExternalContext().getRequestMap().put( PHASE_ID_KEY, event.getPhaseId()); } public static synchronized PhaseId getPhaseId( FacesContext context ) { return (PhaseId)context.getExternalContext().getRequestMap().get( PHASE_ID_KEY ); } public static synchronized boolean performValidation() throws IllegalStateException { FacesContext context = FacesContext.getCurrentInstance(); if( context == null ) { return true; // Out of JSF ( ex. Unit Test ) } PhaseId phaseId = getPhaseId( context ); if( phaseId == null ) { throw new IllegalStateException("Cannot get PhaseId. Check that ValidationPhaseListner is registered to JSF"); } return phaseId.equals( PhaseId.PROCESS_VALIDATIONS ); // Validate only on PROCESS_VALIDATIONS Phase } }
list6. SoftHashMap
package jp.co.exa_corp.validator.hibernate.interceptor;
import java.util.*; import java.lang.ref.*;
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory;
/** SoftHashMap
@author Dr. Heinz Kabutz @see http://archive.devx.com/java/free/articles/Kabutz01/Kabutz01-1.asp Modified for Java 5 Generic by Masato Tsuchiya */ public class SoftHashMap<K,V> extends AbstractMap<K,V> { private static Log log = LogFactory.getLog( SoftHashMap.class );
/** The internal HashMap that will hold the SoftReference. */ private final Map<K,SoftValue<K,V>> hash = new HashMap<K,SoftValue<K,V>>(); /** The FIFO list of hard references, order of last access. */ private final LinkedList<V> hardCache = new LinkedList<V>(); /** Reference queue for cleared SoftReference objects. */ private final ReferenceQueue<V> queue = new ReferenceQueue<V>(); /** The number of "hard" references to hold internally. */ private int HARD_SIZE;
/** We define our own subclass of SoftReference which contains * not only the value but also the key to make it easier to find * the entry in the HashMap after it's been garbage collected. */ private static class SoftValue<K,V> extends SoftReference<V> { private final K key; // always make data member final /** Did you know that an outer class can access private data * members and methods of an inner class? I didn't know that! * I thought it was only the inner class who could access the * outer class's private information. An outer class can also * access private members of an inner class inside its innerclass. */ private SoftValue( V value, K key, ReferenceQueue<V> queue ) { super( value, queue ); this.key = key; } }
public SoftHashMap() { this(100); } public SoftHashMap(int hardSize) { HARD_SIZE = hardSize; }
@Override public V get( Object key ) { V result = null; // We get the SoftReference represented by that key SoftReference<V> soft_ref = hash.get(key); if (soft_ref != null) { /** From the SoftReference we get the value, which can be * null if it was not in the map, or it was removed in * the processQueue() method defined below */ result = soft_ref.get(); if ( result == null) { /** * If the value has been garbage collected, remove the * entry from the HashMap. */ log.trace( key +" is garbage collected"); hash.remove(key); } else { /** * We now add this object to the beginning of the hard * reference queue. One reference can occur more than * once, because lookups of the FIFO queue are slow, so * we don't want to search through it each time to remove * duplicates. */ log.trace( "cache hit: "+ key ); hardCache.addFirst(result); if (hardCache.size() > HARD_SIZE) { // Remove the last entry if list longer than HARD_SIZE hardCache.removeLast(); } } } else { log.trace( "cache miss hit: "+ key ); } return result; }
/** Here we go through the ReferenceQueue and remove garbage * collected SoftValue objects from the HashMap by looking them * up using the SoftValue.key data member. */ @SuppressWarnings("unchecked") private void processQueue() { SoftValue<K,V> sv; while ((sv = (SoftValue<K,V>)queue.poll()) != null) { hash.remove(sv.key); // we can access private data! } } /** Here we put the key, value pair into the HashMap using a SoftValue object. */ @Override public V put( K key, V value) { SoftValue<K,V> r; processQueue(); // throw out garbage collected values first return ( r = hash.put( key, new SoftValue<K,V>(value, key, queue))) != null ? r.get():null; } @Override public V remove( Object key ) { SoftValue<K,V> r; processQueue(); // throw out garbage collected values first return ( r = hash.remove( key )) !=null ? r.get():null; } @Override public void clear() { hardCache.clear(); processQueue(); // throw out garbage collected values hash.clear(); } @Override public int size() { processQueue(); // throw out garbage collected values first return hash.size(); } @Override public Set<Map.Entry<K,V>> entrySet() { // no, no, you may NOT do that!!! GRRR throw new UnsupportedOperationException(); } } |