概要
JSF(TM)における入力データの validation に Hibernate Validator を利用する方法について提案する。入力データの validation はドメインオブジェクトの制約に依存する部分がある。
例えば4桁の数字をチェックする validation は、型が数字であるという制約と、4桁であるという制約に分解できる。4桁である制約がドメインオブジェクトに依存すると考えた場合、この制約はドメインオブジェクトに実装したい。これが、Hibernate Validator を利用しようとした理由である。
ところが、 Hibernate Validator を JSF で使用する仕組みはまだ整備されていない。
Rodrigo Urubatan's Blog [1]に Validator を Intercepter として実装する優れた解決法が示されている(ValidationInterceptor)が、次の理由から十分でない。
1) Domain Object を更新する際に常に validation が実行される不都合がある。JSF におけるValidation Phase に限り、validation を実行したい。
2) validation エラーの表示箇所を探す方法が脆弱である。コンポーネントツーリーを走査して入力タグを探すことは名前の衝突等の原理的な困難がある。
これらの問題の解決策として、次の方法を提案する。
1) は JSF の Validation Phase を検知して Intercepter の Validation を実行する。
2) は 基本的にはJSF 標準に従う。但し、Validation の処理には依存しない汎用的なValidator( DomainValidator ) を作成して、入力データを ValidationInterceptor に導く。
ValidationInterceptor は validation エラーを Exception としてDomainValidatorに戻す。
この方法を用いれば、validation エラーが生じた入力タグを引数で得ることができる。なお、エラーを戻す手段は、ThreadLocal や FacesContext の RequestMap を経由する案もある。
DomainValidatorの実装
JSF の validator は更新の対象である Object の概念がない。ここでは、入力タグの value属性から更新の対象である Object とメソッドを得ることにした。次の例において、
<h:inputText size="2" value="#{select.number}" required="true">
<f:validator validatorId="DomainValidator"/>
</h:inputText>
Expression Language( EL ) で表現された #{select.number} は 名前が "select" であるオブジェクトの メソッド setNumberに引数を与えて実行することを意味する。更新の対象である Object は EL である #{ select } を評価すれば得ることができる。
プログラムをリスト1に示す。Java(TM) の Wrapper クラスに対応するため、コードが複雑になったが、プログラムの要点は EL の評価である。
ValidationInterceptorの実装
リスト2に ValidationInterceptor を示す。 Rodrigo Urubatan's Blog [1]との相違はgetInvalidValues() ではなく getPotentialInvalidValues() したことである。これは、Validation Phase ではオブジェクトの値を更新してはならないからである。
ValidationHelper.performValidation() は Validation Phase において validationを実行させるためにある。その他のフェーズでは validation を実行せずに、ドメインオブジェクトのメソッドを invoke する( pass through )。
日本語で項目名を表示させるため、また、確実に Pointcut を指定するため独自のアノテーションjp.co.exa_corp.validator.Validation を使用した( リスト3 )。
ClassValidator のインスタンスを cache するために、 Dr. Heinz Kabutz が作成したSoftHashMap[2] を Java 5 Generic を使用するように変更して使用した(リスト6)。
ValidationPhaseListener と ValidationHelperの実装
設定
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
AspectJ による Load Time Weaving を使用する場合、aop.xml の設定が必要である。
これはアプリケーションに依存するが、参考に私の場合の設定を示す。
<!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>
留意事項
DomainValidatorでは、入力が Domain Object を変更するようにプログラムしなくてはならない。
入力データを変数に代入してしまうと、validation ができない。このことは、画面に表示するモデルを作成した場合に顕著になる。例を示す。
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 );
}
}
ここで、CartItem は表示のためのモデル、Item は Domain Model である。item.numberには数値範囲を限定する制約があるものとする。注意すべきは setNumber() における item への委譲である。これを次のように書くと DomainValidatorが動作しない。
public void setNumber( int number ) {
this.number = number;
}
リスト1. 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.
// 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.
// 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());
}
}
リスト2. 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;
/**
*
ValidationInterceptor : Validator Using Hibernate Validator
*
* @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;
}
}
リスト3. 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();
}
リスト4. 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;
}
}
リスト5. 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
}
}
リスト6. 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 extends AbstractMap {
private static Log log = LogFactory.getLog( SoftHashMap.class );
/** The internal HashMap that will hold the SoftReference. */
private final Map> hash = new HashMap>();
/** The FIFO list of hard references, order of last access. */
private final LinkedList hardCache = new LinkedList();
/** Reference queue for cleared SoftReference objects. */
private final ReferenceQueue queue = new ReferenceQueue();
/** 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 extends SoftReference {
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 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 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 sv;
while ((sv = (SoftValue)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 r;
processQueue(); // throw out garbage collected values first
return ( r = hash.put( key, new SoftValue(value, key, queue))) != null ? r.get():null;
}
@Override
public V remove( Object key ) {
SoftValue 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> entrySet() {
// no, no, you may NOT do that!!! GRRR
throw new UnsupportedOperationException();
}
}
お問い合わせ