顯示具有 Bean Validation 標籤的文章。 顯示所有文章
顯示具有 Bean Validation 標籤的文章。 顯示所有文章

2013年1月23日 星期三

Applying Bean Validation (JSR303) for Design by Contract

原本method level validation應該在Bean Validation 1.1 (JSR349)才有,但是Hibernate Validator和Spring已經搶先提供這個功能,所以我們現在可以用method validation輕易做到design by contract的precondition和postcondition。

它的運作原理很簡單,Spring利用AOP攔截需要進行validation的method,在method執行的前、後進行驗證,如果有違反condition就拋出exception。

以下是一組設定範例:
Spring XML
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />

<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor" />
Method Validation Sample
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotBlank;
import org.springframework.validation.annotation.Validated;
import com.gss.acl.service.ws.domain.AclUserDetails;

@Validated
public interface AclUserDetailsWebService {

    @NotNull
    AclUserDetails loadUserByUsername(@NotBlank String username);

}
這樣就能在interface中規範condition並強化domain service的完整性,而且將validation邏輯從implementation class中抽離。

2013年1月15日 星期二

Integrate Bean Validation (JSR303) with Struts2 -- SpringI18nInterceptor

Integrate Bean Validation (JSR303) with Struts2 -- Outline
JSR303本身就有提供i18n的功能,但是因為我們要和Struts2整合,所以i18n的部分必須和Struts2同步,這支interceptor的功能就是用來取得Struts2目前的locale,然後傳給Spring,須注意這支interceptor的位置必須在Struts2 i18n interceptor之後。
com.gss.gmo.cao.struts2.interceptor.SpringI18nInterceptor
package com.gss.gmo.cao.struts2.interceptor;

import java.util.Locale;

import org.springframework.context.i18n.LocaleContextHolder;

import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;

/**
 * Populate
 * org.springframework.context.i18n.LocaleContextHolder.setLocale(Locale) by the
 * result from com.opensymphony.xwork2.interceptor.I18nInterceptor.
 * 
 * @author linus_chien
 * 
 */
public class SpringI18nInterceptor extends AbstractInterceptor {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        Locale locale = invocation.getInvocationContext().getLocale();
        LocaleContextHolder.setLocale(locale);
        String result = invocation.invoke();
        return result;
    }

}

Integrate Bean Validation (JSR303) with Struts2 -- Annotation & ValidateMessageFormatter

Integrate Bean Validation (JSR303) with Struts2 -- Outline
我提供了兩個annotation和一個interface,功能如下:
  1. ValidateFieldConfig:若沒設定,預設是驗證整個Action,用此annotation可以只驗證想要驗證的field。
  2. ValidateGroupConfig:搭配JSR303的group使用。
  3. ValidateMessageFormatter:當Action有implement此interface時,進行callback,讓Action自己決定error message format。
com.gss.gmo.cao.struts2.interceptor.validation.ValidateFieldConfig
package com.gss.gmo.cao.struts2.interceptor.validation;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
 * Used by SpringValidationInterceptor. The intercepter will validate entire
 * action if ValidateFieldConfig not presented, or all fields one by one
 * configured in ValidateFieldConfig.
 *
 * @author linus_chien
 *
 */
@Documented
@Target(value = METHOD)
@Retention(value = RUNTIME)
public @interface ValidateFieldConfig {

    /**
     * Field names in action to validate.
     *
     * @return
     */
    String[] fields() default {};

}
com.gss.gmo.cao.struts2.interceptor.validation.ValidateGroupConfig
package com.gss.gmo.cao.struts2.interceptor.validation;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.groups.Default;

/**
 * Applying Bean Validation Groups.
 *
 * @author linus_chien
 *
 */
@Documented
@Target(value = METHOD)
@Retention(value = RUNTIME)
public @interface ValidateGroupConfig {

    /**
     * @return
     */
    Class<?>[] groups() default { Default.class };

}
com.gss.gmo.cao.struts2.interceptor.validation.ValidateMessageFormatter
package com.gss.gmo.cao.struts2.interceptor.validation;

public interface ValidateMessageFormatter {

    String format(String fieldName, String errorMessage);

}

Integrate Bean Validation (JSR303) with Struts2 -- Configuration

Integrate Bean Validation (JSR303) with Struts2 -- Outline
有三個主要的設定,一是設定Validator instance,implementation可以使用Hibernate Validator;另一個是設定SpringValidationInterceptor和SpringI18nInterceptor,三者我們都交給Spring控管;最後在struts.xml中設定interceptor,並套用到Action就可以了。
Spring XML
<bean id="gmo.cao.validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
Spring Configuration
package com.gss.gmo.cao.struts2.spring;

import javax.validation.Validator;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.gss.gmo.cao.struts2.interceptor.SpringI18nInterceptor;
import com.gss.gmo.cao.struts2.interceptor.SpringValidationInterceptor;
import com.opensymphony.xwork2.interceptor.Interceptor;

@Configuration
public class InterceptorConfiguration {

    final static String EXCLUDE_METHODS = "input, back, cancel, browse";

    @Bean(name = "gmo.cao.springI18n")
    Interceptor getSpringI18nInterceptor() {
        return new SpringI18nInterceptor();
    }

    @Bean(name = "gmo.cao.springValidation")
    @Autowired
    Interceptor createSpringValidationInterceptor(@Qualifier("gmo.cao.validator") Validator validator) {
        SpringValidationInterceptor interceptor = new SpringValidationInterceptor();
        interceptor.setExcludeMethods(EXCLUDE_METHODS);
        interceptor.setValidator(validator);
        return interceptor;
    }

}
struts.xml
<interceptor name="springI18n" class="gmo.cao.springI18n" />
<interceptor name="springValidation" class="gmo.cao.springValidation" />

Integrate Bean Validation (JSR303) with Struts2 -- Struts2 Action Sample

Integrate Bean Validation (JSR303) with Struts2 -- Outline
最後是一個套用JSR303的Action sample code。
代碼:
package com.gss.gmo.cao.struts2.interceptor;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

import lombok.Setter;

import com.gss.gmo.cao.struts2.interceptor.validation.ValidateFieldConfig;
import com.gss.gmo.cao.struts2.interceptor.validation.ValidateGroupConfig;
import com.gss.gmo.cao.struts2.interceptor.validation.ValidateMessageFormatter;
import com.opensymphony.xwork2.ActionSupport;

public class UserAction extends ActionSupport implements ValidateMessageFormatter {

    private static final long serialVersionUID = 1L;

    @Setter
    @NotNull
    private String username;

    @Setter
    @Min(1)
    @Max(99)
    private int age;

    @Setter
    private UserAction user;

    @ValidateGroupConfig
    public String execute() {
        return SUCCESS;
    }

    @ValidateFieldConfig(fields = "user")
    public String validateFieldConfig() {
        return SUCCESS;
    }

    @Override
    public String format(String fieldName, String errorMessage) {
        return fieldName + " - " + errorMessage;
    }

}

Integrate Bean Validation (JSR303) with Struts2 -- SpringValidationInterceptor

Integrate Bean Validation (JSR303) with Struts2 -- Outline
整個整合的核心就在這支interceptor裡面,將整個Action傳給Validator進行驗證,若有錯誤就將錯誤塞到FieldError裡面,對熟悉Struts2的人來說應該不難。須額外注意的是如果Action是CGLIB proxy的話,需要轉換成target object進行驗證,因為Spring AOP生成的proxy object裡面field並不會有值。
com.gss.gmo.cao.struts2.interceptor.SpringValidationInterceptor
package com.gss.gmo.cao.struts2.interceptor;

import static org.apache.commons.lang.StringUtils.isBlank;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.springframework.aop.support.AopUtils.getTargetClass;
import static org.springframework.aop.support.AopUtils.isCglibProxy;
import static org.springframework.util.ReflectionUtils.findField;
import static org.springframework.util.ReflectionUtils.getField;
import static org.springframework.util.ReflectionUtils.makeAccessible;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;

import lombok.Setter;

import org.apache.struts2.interceptor.validation.SkipValidation;

import com.gss.gmo.cao.struts2.interceptor.validation.ValidateFieldConfig;
import com.gss.gmo.cao.struts2.interceptor.validation.ValidateGroupConfig;
import com.gss.gmo.cao.struts2.interceptor.validation.ValidateMessageFormatter;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
import com.opensymphony.xwork2.util.reflection.ReflectionProvider;

/**
 * Use Spring and JSR 303 to validate action data.
 *
 * @author linus_chien
 *
 */
public class SpringValidationInterceptor extends MethodFilterInterceptor {

    private static final long serialVersionUID = 1L;

    @Setter
    private Validator validator;

    private ReflectionProvider reflectionProvider;

    @Inject
    public void setReflectionProvider(ReflectionProvider prov) {
        this.reflectionProvider = prov;
    }

    @Override
    protected String doIntercept(ActionInvocation invocation) throws Exception {
        Object originalAction = invocation.getAction();
        if (!(originalAction instanceof ActionSupport)) {
            return invocation.invoke();
        }

        Object targetAction = originalAction;
        if (isCglibProxy(originalAction)) {
            targetAction = getTargetClass(originalAction).newInstance();
            reflectionProvider.copy(originalAction, targetAction, invocation.getInvocationContext().getContextMap(), null, null);
        }

        Method method = targetAction.getClass().getDeclaredMethod(invocation.getProxy().getMethod(), new Class[] {});
        if (method.isAnnotationPresent(SkipValidation.class)) {
            return invocation.invoke();
        }

        ActionSupport originalActionSupport = (ActionSupport) originalAction;
        ActionSupport targetActionSupport = (ActionSupport) targetAction;

        ValidateGroupConfig groupConfig = method.getAnnotation(ValidateGroupConfig.class);
        Class<?>[] groups = {};
        if (groupConfig != null) {
            groups = groupConfig.groups();
        }

        ValidateFieldConfig fieldConfig = method.getAnnotation(ValidateFieldConfig.class);
        if (fieldConfig != null) {
            for (String field : fieldConfig.fields()) {
                if (isNotBlank(field)) {
                    validate(originalActionSupport, targetActionSupport, field, groups);
                }
            }
        } else {
            validate(originalActionSupport, targetActionSupport, "", groups);
        }

        return invocation.invoke();
    }

    /**
     * @param originalActionSupport
     * @param targetActionSupport
     * @param propertyPathPrefix
     * @param groups
     */
    private void validate(ActionSupport originalActionSupport, ActionSupport targetActionSupport, String propertyPathPrefix, Class<?>... groups) {
        Object target;
        if (isBlank(propertyPathPrefix)) {
            target = targetActionSupport;
        } else {
            Field targetField = findField(targetActionSupport.getClass(), propertyPathPrefix);
            if (targetField == null) {
                return;
            }
            makeAccessible(targetField);
            target = getField(targetField, targetActionSupport);
            if (target == null) {
                return;
            }
        }

        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(target, groups);

        for (ConstraintViolation<Object> constraintViolation : constraintViolations) {
            String propertyPath = constraintViolation.getPropertyPath().toString();
            if (isNotBlank(propertyPathPrefix)) {
                propertyPath = propertyPathPrefix + "." + propertyPath;
            }
            String errorMessage = constraintViolation.getMessage();
            if (originalActionSupport instanceof ValidateMessageFormatter) {
                errorMessage = ((ValidateMessageFormatter) originalActionSupport).format(originalActionSupport.getText(propertyPath, ""), errorMessage);
            }
            originalActionSupport.addFieldError(propertyPath, errorMessage);
        }
    }

}

Integrate Bean Validation (JSR303) with Struts2 -- Outline

許多MVC Framework都已經將JSR303納為default form validation的方式,但是Struts2卻遲遲未做,所以我得自己來整合這一段,步驟頗繁瑣,所以分為幾篇來說明,這裡只列出大綱。

2013年1月10日 星期四

符合Bean Validation規格的統一編號驗證 (JSR303)

Annotation
package com.gss.gmo.cao.validator.constraints;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
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;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

import com.gss.gmo.cao.validator.constraints.impl.UnifiedBusinessNumberValidator;

/**
 * The annotated element has to represent a valid unified business number.
 * 
 * @author linus_chien
 * 
 */
@Documented
@Constraint(validatedBy = UnifiedBusinessNumberValidator.class)
@Target(value = { METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(value = RUNTIME)
public @interface UnifiedBusinessNumber {

    /**
     * Default key:
     * com.gss.gmo.cao.validator.constraints.UnifiedBusinessNumber.message.
     * 
     * @return
     */
    String message() default "{com.gss.gmo.cao.validator.constraints.UnifiedBusinessNumber.message}";

    /**
     * Default {}.
     * 
     * @return
     */
    Class<?>[] groups() default {};

    /**
     * Default {}.
     * 
     * @return
     */
    Class<? extends Payload>[] payload() default {};

    /**
     * Defines several @UnifiedBusinessNumber annotations on the same element.
     * 
     * @author linus_chien
     * 
     */
    @Documented
    @Target(value = { METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
    @Retention(value = RUNTIME)
    public @interface List {
        /**
         * @return
         */
        UnifiedBusinessNumber[] values();
    }

}
ConstraintValidator Implementation
package com.gss.gmo.cao.validator.constraints.impl;

import java.util.regex.Pattern;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import com.gss.gmo.cao.validator.constraints.UnifiedBusinessNumber;

/**
 * 營利事業統一編號檢查程式 可至 http://www.etax.nat.gov.tw/ 查詢營業登記資料
 * 
 * @author linus_chien
 * 
 */
public class UnifiedBusinessNumberValidator implements ConstraintValidator<UnifiedBusinessNumber, String> {

    /**
     * Number pattern.
     */
    public static final Pattern NUMBER_PATTERN = Pattern.compile("^[0-9]{8}$");

    /**
     * Calculate weight.
     */
    public static final char[] WEIGHT = "12121241".toCharArray();

    @Override
    public void initialize(UnifiedBusinessNumber annotation) {
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        if (!NUMBER_PATTERN.matcher(value).matches()) {
            return false;
        }

        char[] numbers = value.toCharArray();

        boolean isSeven = (numbers[6] == '7'); // 第七個數是否為七

        int sum = 0;
        for (int i = 0; i < 8; i++) {
            if (i == 6 && isSeven) {
                continue;
            }
            int singleValue = (numbers[i] - '0') * (WEIGHT[i] - '0');
            sum += (singleValue / 10) + (singleValue % 10);
        }

        if (isSeven) {
            if ((sum % 10) == 0 || ((sum + 1) % 10) == 0) {
                return true;
            }
        } else {
            if ((sum % 10) == 0) {
                return true;
            }
        }

        return false;

    }

}

符合Bean Validation規格的身分證字號驗證 (JSR303)

Annotation
package com.gss.gmo.cao.validator.constraints;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
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;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

import com.gss.gmo.cao.validator.constraints.impl.IdentificationNumberValidator;

/**
 * The annotated element has to represent a valid identification number.
 *
 * @author linus_chien
 *
 */
@Documented
@Constraint(validatedBy = IdentificationNumberValidator.class)
@Target(value = { METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(value = RUNTIME)
public @interface IdentificationNumber {

    /**
     * Default key:
     * com.gss.gmo.cao.validator.constraints.IdentificationNumber.message.
     *
     * @return
     */
    String message() default "{com.gss.gmo.cao.validator.constraints.IdentificationNumber.message}";

    /**
     * Default {}.
     *
     * @return
     */
    Class<?>[] groups() default {};

    /**
     * Default {}.
     *
     * @return
     */
    Class<? extends Payload>[] payload() default {};

    /**
     * Defines several @IdentificationNumber annotations on the same element.
     *
     * @author linus_chien
     *
     */
    @Documented
    @Target(value = { METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
    @Retention(value = RUNTIME)
    public @interface List {
        /**
         * @return
         */
        IdentificationNumber[] values();
    }

}
ConstraintValidator Implementation
package com.gss.gmo.cao.validator.constraints.impl;

import java.util.regex.Pattern;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import com.gss.gmo.cao.validator.constraints.IdentificationNumber;

/**
 * 身份證字號檢查程式,身份證字號規則: 字母(ABCDEFGHJKLMNPQRSTUVXYWZIO)對應一組數(10~35),
 * 令其十位數為X1,個位數為X2;( 如A:X1=1 , X2=0 );D?表示2~9數字 Y = X1 + 9*X2 + 8*D1 + 7*D2 +
 * 6*D3 + 5*D4 + 4*D5 + 3*D6 + 2*D7+ 1*D8 + D9 如Y能被10整除,則表示該身份證號碼為正確,否則為錯誤。
 * 臺北市(A)、臺中市(B)、基隆市(C)、臺南市(D)、高雄市(E)、臺北縣(F)、
 * 宜蘭縣(G)、桃園縣(H)、嘉義市(I)、新竹縣(J)、苗栗縣(K)、臺中縣(L)、
 * 南投縣(M)、彰化縣(N)、新竹市(O)、雲林縣(P)、嘉義縣(Q)、臺南縣(R)、
 * 高雄縣(S)、屏東縣(T)、花蓮縣(U)、臺東縣(V)、金門縣(W)、澎湖縣(X)、 陽明山(Y)、連江縣(Z)
 *
 * @author linus_chien
 *
 */
public class IdentificationNumberValidator implements ConstraintValidator<IdentificationNumber, String> {

    /**
     * Id pattern.
     */
    public static final Pattern ID_PATTERN = Pattern.compile("[A-Z][12]\\d{8}");

    /**
     * The first chars of a valid id.
     */
    public static final String ID_FIRST_CHARS = "ABCDEFGHJKLMNPQRSTUVXYWZIO";

    @Override
    public void initialize(IdentificationNumber annotation) {
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        value = value.toUpperCase();

        if (!ID_PATTERN.matcher(value).matches()) {
            return false;
        }

        char[] numbers = value.toCharArray();

        int code = ID_FIRST_CHARS.indexOf(numbers[0]) + 10;
        int sum = (int) (code / 10) + 9 * (code % 10);

        for (int i = 1; i <= 8; i++) {
            sum += ((numbers[i] - '0') * (9 - i));
        }

        sum += (numbers[9] - '0');

        if (sum % 10 == 0) {
            return true;
        } else {
            return false;
        }
    }

}