Archive for 2015年3月26日
JSF の相関項目チェック by Bean Validation
このエントリは、半分以上お遊びですのであまり詳しくは説明しません。ご興味ある方はお試しください。JSF で複数入力項目がある場合の相関チェックをする場合、通常は JSF のコンポーネントにバインドさせてバリデーション・フェーズで検証を行っていましたが(ご参照:本エントリの一番下に例を記載してますがそちらの方が楽です)、今回、Bean Validation、カスタム Bean Validation さらに CDI と PhasesListner を駆使して複数入力項目がある場合の相関チェックをしてみました。
JSF では直接 Bean Validation のクラスレベルの検証ができないので、無理矢理クラスレベルでチェクをするために、PhasesListener 内で ValidatorFactory をインジェクトして、validateメソッドに渡す事で Person クラスをValidate しています。おもしろいのが、JSF のライフサイクルで「モデル値の適用」フェーズ後は Person に代入された値が @Inject Instance<Person> でインジェクトできるようになるので、直接 Person としていじくれるようになる事です。
JSF の Facelets の内容
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"> <h:head> <title>Facelet Title</title> </h:head> <h:body> <h:form prependId="false"> <h:outputLabel id="birthDay" value="Input Birth Day"/> <h:inputText id="inputYear" value="#{personManaged.person.birthYear}"/>/ <h:inputText id="inputMonth" value="#{personManaged.person.birthMonth}"/>/ <h:inputText id="inputDay" value="#{personManaged.person.birthDay}"/><br/> <h:outputLabel id="age" value="Input Age"/> <h:inputText id="inputAge" value="#{personManaged.person.age}"/> <h:commandButton value="Check Multi Value" action="#{personManaged.execSubmitButton()}"/> <h:messages/> </h:form> </h:body> </html>
faces-config.xml の内容
<?xml version='1.0' encoding='UTF-8'?> <faces-config version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd"> <lifecycle> <phase-listener>jp.co.oracle.jdbcrealm.cdis.phaselisteners.ValiationPhaseListener</phase-listener> </lifecycle> </faces-config>
Facelets のバッキング・ビーン
package jp.co.oracle.jdbcrealm.cdis; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.inject.Named; import jp.co.oracle.jdbcrealm.customvalidations.Person; @Named(value = "personManaged") @RequestScoped public class PersonBirthDayValidatorBackingBean { @Inject private Person person; public Person getPerson() { return person; } public void setPerson(Person person) { this.person = person; } public void execSubmitButton() { System.out.println(person.getBirthYear() + "/" + person.getBirthMonth() + "/" + person.getBirthDay() + "\t" + person.getAge()); } }
Person クラス
package jp.co.oracle.jdbcrealm.customvalidations; import java.math.BigDecimal; import javax.enterprise.context.RequestScoped; import javax.validation.constraints.Digits; import javax.validation.constraints.NotNull; @RequestScoped @PersonClassLevelValidator // <---- カスタム・バリデータ public class Person { @NotNull private Integer name; @NotNull @Digits(integer = 3,fraction = 0) private Integer age; @NotNull @Digits(integer = 4,fraction = 0) private Integer birthYear; @NotNull @Digits(integer = 2,fraction = 0) private Integer birthMonth; @NotNull @Digits(integer = 2,fraction = 0) private Integer birthDay; /** * @return the name */ public Integer getName() { return name; } /** * @param name the name to set */ public void setName(Integer name) { this.name = name; } /** * @return the age */ public Integer getAge() { return age; } /** * @param age the age to set */ public void setAge(Integer age) { this.age = age; } /** * @return the birthYear */ public Integer getBirthYear() { return birthYear; } /** * @param birthYear the birthYear to set */ public void setBirthYear(Integer birthYear) { this.birthYear = birthYear; } /** * @return the birthMonth */ public Integer getBirthMonth() { return birthMonth; } /** * @param birthMonth the birthMonth to set */ public void setBirthMonth(Integer birthMonth) { this.birthMonth = birthMonth; } /** * @return the birthDay */ public Integer getBirthDay() { return birthDay; } /** * @param birthDay the birthDay to set */ public void setBirthDay(Integer birthDay) { this.birthDay = birthDay; } }
Person クラスのバリデータ(相関チェックの実装)
package jp.co.oracle.jdbcrealm.customvalidations; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.time.DateTimeException; import java.time.LocalDate; import java.time.Period; import javax.validation.Constraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import javax.validation.Payload; @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) @Retention(RUNTIME) @Constraint(validatedBy = PersonClassLevelValidator.PersonClassValidtor.class) @Documented public @interface PersonClassLevelValidator { String message() default " Invalid input of the Birthday or Age"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; class PersonClassValidtor implements ConstraintValidator<PersonClassLevelValidator, Person> { PersonClassLevelValidator constraintAnnotation; @Override public void initialize(PersonClassLevelValidator constraintAnnotation) { this.constraintAnnotation = constraintAnnotation; } @Override public boolean isValid(Person person, ConstraintValidatorContext context) { if ( person == null) return true; // Person として直接扱えるので色々楽 int age = person.getAge(); int birthYear = person.getBirthYear(); int birthMonth = person.getBirthMonth(); int birthDay = person.getBirthDay(); LocalDate birthDate; try { birthDate = LocalDate.of(birthYear, birthMonth, birthDay); } catch (DateTimeException de) { return false; } LocalDate today = LocalDate.now(); Period period = Period.between(birthDate, today); return age == period.getYears(); } } }
JSF の PhasesListener でモデル値適用後に Person をバリデート
package jp.co.oracle.jdbcrealm.cdis.phaselisteners; import java.util.Set; import javax.faces.application.FacesMessage; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseId; import javax.faces.event.PhaseListener; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.validation.ConstraintViolation; import javax.validation.ValidatorFactory; import javax.validation.groups.Default; import jp.co.oracle.jdbcrealm.customvalidations.Person; import jp.co.oracle.jdbcrealm.customvalidations.PersonClassLevelValidator; public class ValiationPhaseListener implements PhaseListener { @Override public PhaseId getPhaseId() { return PhaseId.UPDATE_MODEL_VALUES; } @Inject ValidatorFactory validFactory; @Inject javax.enterprise.inject.Instance<Person> instanceOfPerson; @Override public void afterPhase(PhaseEvent event) { //どこからリクエストが来たかを検証 // ここの実装を綺麗にしたかったが難しかった。 FacesContext fContext = event.getFacesContext(); ExternalContext extContext = fContext.getExternalContext(); String url = ((HttpServletRequest) extContext.getRequest()).getRequestURL().toString(); if (!url.contains("PersonBirthDayValidate.xhtml")) { return; } //モデルに値が適用された後(PhaseId.UPDATE_MODEL_VALUES)は、Person に値が代入されているためインジェクト可能 Person person = instanceOfPerson.get(); // Execute only PersonClassLevelValidator(Class-Constraint) Validation Set<ConstraintViolation<Person>> violations = validFactory.getValidator().validate(person, Default.class); //Bean Valiadtion の検証結果 violations.stream() .filter(consts -> consts.getConstraintDescriptor().getAnnotation() instanceof PersonClassLevelValidator) .map(violation -> violation.getMessage()) .findFirst() .ifPresent((String errMsg) -> { FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, errMsg, null); fContext.addMessage("ERROR", facesMessage); fContext.validationFailed(); fContext.renderResponse(); //SKIP lifecycle }); } @Override public void beforePhase(PhaseEvent event) { } }
ちなみに、従来の JSF コンポーネントバインドを使った相関チェックのやり方はこんなかんじ
<f:view> <f:metadata> <f:viewAction action="#{indexManaged.checkArgument()}" onPostback="true" phase="PROCESS_VALIDATIONS" /> </f:metadata> <h:head> <title>Facelet Title</title> </h:head> <h:body> <h:form> <h:inputText id="text1" value="#{indexManaged.text1}" binding="#{indexManaged.bindComp1}"/> <h:inputText id="text2" value="#{indexManaged.text2}" binding="#{indexManaged.bindComp2}"/> <h:messages id="error_message" style="color:red;margin:8px;"/> <h:commandButton value="OK" action="#{indexManaged.pushButton()}"/> </h:form> </h:body> </f:view>
package jp.co.oracle.jsf22.multivalidate; import javax.inject.Named; import javax.enterprise.context.RequestScoped; import javax.faces.application.FacesMessage; import javax.faces.context.FacesContext; import javax.faces.component.html.HtmlInputText; @Named(value = "indexManaged") @RequestScoped public class IndexManaged { private String text1; private String text2; private HtmlInputText bindComp1; private HtmlInputText bindComp2; /** * Creates a new instance of IndexManaged */ public IndexManaged() { } /** * @return the text1 */ public String getText1() { return text1; } /** * @param text1 the text1 to set */ public void setText1(String text1) { this.text1 = text1; } /** * @return the text2 */ public String getText2() { return text2; } /** * @param text2 the text2 to set */ public void setText2(String text2) { this.text2 = text2; } public String pushButton() { return ""; } /** * @return the bindComp1 */ public HtmlInputText getBindComp1() { return bindComp1; } /** * @param bindComp1 the bindComp1 to set */ public void setBindComp1(HtmlInputText bindComp1) { this.bindComp1 = bindComp1; } /** * @return the bindComp2 */ public HtmlInputText getBindComp2() { return bindComp2; } /** * @param bindComp2 the bindComp2 to set */ public void setBindComp2(HtmlInputText bindComp2) { this.bindComp2 = bindComp2; } public void checkArgument() { String val1 = bindComp1.getValue() ; String val2 = bindComp2.getValue() ; if (!val1.equals(val2)){ FacesMessage msg = new FacesMessage(FacesMessage.SEVERITY_ERROR, "入力内容が不正です。", "テキスト・フィールドに正しい値が入力されていません。"); FacesContext.getCurrentInstance().addMessage(null, msg); } } }