システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る

Paraya と Java EE 8 で Hello

EEなんだから、やっぱりParayaだよね。

ということで、この記事のParaya版

vermeer.hatenablog.jp

Hello しかしていないから、本当にこれが正しいのか全く分からないけれど。。

ParayaはEサーバーなので、pomでの依存解決や、web.xmlなどの記述量が減りますね。

依存関係

pom.xml

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

これだけ。

設定

WEB-INF/beans.xml

CDIのバージョンを2.0にしておく。
しないといけないか、分からないけど理屈上はしておくべきだと思うので。

<?xml version="1.0" encoding="UTF-8"?>
<beans 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/beans_2_0.xsd"
       bean-discovery-mode="annotated">
</beans>

WEB-INF/web.xml

Servletのバージョンを4.0にしておく。
しないといけないか、分からないけど理屈上はしておくべきだと思うので。

<?xml version="1.0" encoding="UTF-8"?>
<web-app
    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-app_4_0.xsd"
    version="4.0">

    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>

    <context-param>
        <param-name>javax.faces.validator.DISABLE_DEFAULT_BEAN_VALIDATOR</param-name>
        <param-value>true</param-value>
    </context-param>

    <context-param>
        <param-name>javax.faces.SEPARATOR_CHAR</param-name>
        <param-value>-</param-value>
    </context-param>

    <welcome-file-list>
        <welcome-file>index.xhtml</welcome-file>
    </welcome-file-list>

    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>

</web-app>

Code

Bitbucket

さいごに

まだ手探り。

とりあえずEEで色々するのは Tomcatじゃなくて、ちゃんとEEサーバーで色々と試していこうかな。

というわけで Hello Java EE 8

2018/4/25 追記
NetBeans8.2で開発をするんだったら、とりあえずJava EE 7までにしておいた方が良さそうなので、この雛型は使わない。

Tomcat と Java EE 7(JSF2.2) で Hello

余分なものを除外した雛型みたいなもの。

画面遷移も なーんにもない、本当に「Hello」としか出力しないもの。

以前、作った時のpomと今回のpomは微妙に違ったりも。

しかも、Java EE 8 がリリースされているのに、Java EE 7(笑)*1

とにかく、これでJSFで実験するための土台は出来たかな。

依存関係

pom.xml

<dependencies>

    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-web-api</artifactId>
        <version>7.0</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>javax.faces</groupId>
        <artifactId>javax.faces-api</artifactId>
        <version>2.2</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>org.glassfish</groupId>
        <artifactId>javax.faces</artifactId>
        <version>2.2.17</version>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>javax.enterprise</groupId>
        <artifactId>cdi-api</artifactId>
        <version>1.2</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>org.jboss.weld.servlet</groupId>
        <artifactId>weld-servlet</artifactId>
        <version>2.4.7.Final</version>
        <scope>runtime</scope>
    </dependency>

</dependencies>

設定

META-INF/context.xml

<?xml version="1.0" encoding="UTF-8"?>
<Context antiJARLocking="true" antiResourceLocking="true" path="/jsf-base-tomcat">
    <Resource auth="Container" factory="org.jboss.weld.resources.ManagerObjectFactory" name="BeanManager" type="javax.enterprise.inject.spi.BeanManager"/>
</Context>

WEB-INF/beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans 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/beans_1_2.xsd"
       bean-discovery-mode="annotated">
</beans>

WEB-INF/faces-config.xml

<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="2.2"
              xmlns="http://java.sun.com/xml/ns/javaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_2.xsd">
</faces-config>

WEB-INF/web.xml

雛型と言いつつ、web.xmlは記載多め。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" 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-app_3_1.xsd">
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>

    <context-param>
        <param-name>javax.faces.validator.DISABLE_DEFAULT_BEAN_VALIDATOR</param-name>
        <param-value>true</param-value>
    </context-param>

    <context-param>
        <param-name>javax.faces.SEPARATOR_CHAR</param-name>
        <param-value>-</param-value>
    </context-param>

    <filter>
        <filter-name>SetCharacterEncoding</filter-name>
        <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>ignore</param-name>
            <param-value>false</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>SetCharacterEncoding</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>Conversation</filter-name>
        <filter-class>org.jboss.weld.servlet.ConversationFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>Conversation</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <listener>
        <listener-class>org.jboss.weld.environment.servlet.Listener</listener-class>
    </listener>

    <resource-env-ref>
        <resource-env-ref-name>BeanManager</resource-env-ref-name>
        <resource-env-ref-type>javax.enterprise.inject.spi.BeanManager</resource-env-ref-type>
    </resource-env-ref>

    <welcome-file-list>
        <welcome-file>index.xhtml</welcome-file>
    </welcome-file-list>

    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>

</web-app>

Code

Bitbucket

Java EE 8版
Bitbucket

*1:2018/4/19 Java EE 8版も作ってみました。Codeにも追記

BeanValidationのメッセージ遅延変換

過去記事の

vermeer.hatenablog.jp

vermeer.hatenablog.jp

vermeer.hatenablog.jp

と、ResourceBundleの扱いを追加した

vermeer.hatenablog.jp

を まとめ直してライブラリを作成しました。

過去記事との違い

ライブラリにするにあたって、過去の記事の実装との相違点は以下

  • 参照リソースを複数指定できるようにする

  • メッセージ変換都度 Resourceのパスを渡すようにせず、Factoryで指定する

  • LocaleとResourceのパスを渡すタイミングを分ける

  • FormのValidationは適時実装するものとしてライブラリからは一旦除外する。

メッセージ変換の使い方(参考)

ざっくり示すと以下のような感じ。

Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

Set<ConstraintViolation<TargetView>> results = validator.validate(view, FormValidation.class);


// メッセージ変換に使用するResourceBundleを指定します
MessageInterpolatorFactory interpolatorFactory
              = MessageInterpolatorFactory.of("Messages", "FormMessages", "FormLabels");

// 複数のリソースを指定したい場合
//        MessageInterpolatorFactory interpolatorFactory
//                                  = MessageInterpolatorFactory.of(
//                        new String[]{"Messages", "Messages2"},
//                        new String[]{"FormMessages", "FormMessages2"},
//                        new String[]{"FormLabels", "FormLabels2"});

// 変換インスタンスを生成します(ロケールは未指定)
MessageInterpolator interpolator = interpolatorFactory.create();

// 変換インスタンスを生成します(ロケールを指定)
// MessageInterpolator interpolator = interpolatorFactory.create(Locale.JAPAN);

for (ConstraintViolation<TargetView> result : results) {
    // 検証結果からメッセージを変換します.
    String convertedMessage = interpolator.toMessage(result);
    System.out.println(convertedMessage);
}

その他、実際のAnnotationとの組み合わせについては、テストコードを参照していただければと思います。

Code

Bitbucket

さいごに

これだけだと実際のところ不十分で、JSFなどWebシステムでメッセージを出力するという具体的な使用感を掴みながら改修をしていくことになると思います。

複数のResourceBundleを参照できるようにする

はじめに

そもそもResourceBundle#getBundleに対して複数のリソースを読み込ませたいという要求自体があまりないのかもしれません。

もしそれをしたいのであれば、ListなりにResourceBundleを保持しておいて検索すれば事足りると思います。

ですが、BeanValidationでメッセージ出力をしているときに遭遇したのはResourceBundleをインターフェースとしたクラスを引数として渡さないといけないというケースでした。

色々なやり方があるとは思いますが、私はnewBundleを拡張するやり方でやってみました。

具体的には

vermeer.hatenablog.jp

で自作したライブラリの拡張です。

実装

方式としては、参照資産を全て1つのstreamに結合して読み込ませる、というだけです。

今回の実験で得た知識はSequenceInputStreamです。

なんと、JDK1.0からのクラスです。知りませんでした。

それをリソースファイル分、再帰処理で結合していきました(#concateInputStrem)。

ちょっとした工夫としては、リソースファイルが改行で終わっていないケースを考慮して 結合時に改行情報を差し込むようにしたところでしょうか。

protected ResourceBundle newBundleProperties(String baseName, Locale locale, ClassLoader loader, boolean reload)
        throws IllegalAccessException, InstantiationException, IOException {
    InputStream stream = readBaseNames(baseName, locale, loader, reload);
    ResourceBundle bundle = null;
    if (stream != null) {
        try {
            if (this.charCode != null) {
                bundle = new PropertyResourceBundle(new InputStreamReader(stream, this.charCode));
            } else {
                bundle = new PropertyResourceBundle(stream);
            }
        } finally {
            stream.close();
        }
    }
    return bundle;
}

InputStream readBaseNames(String baseName, Locale locale, ClassLoader loader, boolean reload) throws IOException {
    String[] _baseNames = baseName.split(",");
    List<InputStream> propertiesInputStreams = new ArrayList<>();
    for (String _baseName : _baseNames) {
        InputStream propertiesInputStream = readProperties(_baseName.trim(), locale, loader, reload);
        /* propertiesの結合時に使用する改行文字 */
        InputStream lineSeparator = new ByteArrayInputStream(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
        if (propertiesInputStream != null) {
            InputStream inputStreamWithLineSeparator = new SequenceInputStream(propertiesInputStream, lineSeparator);
            propertiesInputStreams.add(inputStreamWithLineSeparator);
        }
    }

    if (propertiesInputStreams.isEmpty()) {
        return null;
    }

    Iterator<InputStream> inputStreamsIterator = propertiesInputStreams.iterator();
    InputStream inputStream = inputStreamsIterator.next();
    if (propertiesInputStreams.size() == 1) {
        return inputStream;
    }
    return concateInputStrem(inputStreamsIterator, inputStream);
}

InputStream readProperties(String baseName, Locale locale, ClassLoader loader, boolean reload) throws IOException {
    String bundleName = toBundleName(baseName, locale);
    final String resourceName = (bundleName.contains("://"))
                                ? null
                                : toResourceName(bundleName, "properties");
    if (resourceName == null) {
        return null;
    }
    final ClassLoader classLoader = loader;
    final boolean reloadFlag = reload;
    InputStream stream = null;
    try {
        stream = AccessController.doPrivileged((PrivilegedExceptionAction<InputStream>) () -> {
            InputStream is = null;
            if (reloadFlag) {
                URL url = classLoader.getResource(resourceName);
                if (url != null) {
                    URLConnection connection = url.openConnection();
                    if (connection != null) {
                        // Disable caches to get fresh data for reloading.
                        connection.setUseCaches(false);
                        is = connection.getInputStream();
                    }
                }
            } else {
                is = classLoader.getResourceAsStream(resourceName);
            }
            return is;
        });
    } catch (PrivilegedActionException e) {
        throw (IOException) e.getException();
    }
    return stream;
}

/**
 * InputStreamを結合して返却します.
 *
 * @param inputStreamsIterator 参照資産のイテレーター
 * @param concatedInputStream 結合先となるInputStream
 * @return 結合したInputStream
 */
InputStream concateInputStrem(Iterator<InputStream> inputStreamsIterator, InputStream concatedInputStream) throws IOException {
    if (inputStreamsIterator.hasNext() == false) {
        return concatedInputStream;
    }
    InputStream inputStream = inputStreamsIterator.next();
    SequenceInputStream sequenceInputStream = new SequenceInputStream(concatedInputStream, inputStream);
    return concateInputStrem(inputStreamsIterator, sequenceInputStream);
}

Code

Bitbucket

さいごに

ResoueceBundleそのものを拡張する、というやり方も試したりしたのですが、思ったようにできませんでした。

このやり方であれば特殊なことはしていないので(単純に読み込むstreamを結合しただけ)、コードのボリュームの割には単純なやり方なように思います。

ちょっと迷ったのは、Controlに追加したいPropertyFileを指定する、というやり方です。 こちらの案だったら、引数の型を配列なり、リストなりにして「複数指定していますよ」ということをメソッドで表現できます。

最終的に本方式にしたのは「ファイルの指定箇所が分散すると3日後の自分が忘れそうだ」というのが大きく、もう1つはControlをResourceBundleの生成都度置き換えるという実装思想が、しっくり来なかったからです。

Labelが無くても良いようにしてみた

vermeer.hatenablog.jp

の続きです。

AnnotationでLabelを付与するという仕組みにしたわけですが、よくよく考えてみるとFormObjectというインターフェースをもっているのだから、そこで対象クラスを判断してPropertyに登録した表示項目名を参照するようにしても良いのでは?と思いました。

もちろん、Labelは残しておきますが、そういうのもアリかな?ということです。

ということで試しにやってみました。

方式

  • FormObjectインターフェースを実装したクラスについては「変換する対象となるラベル情報」をResourceBundleから取得する。

  • 独自の表示項目名を指定したい場合は@Labelを使う。

実装

Interfaceを処理対象に

フォームクラスのインターフェースの確認およびキー値を編集

public class LabelConverter {

    private final ConstraintViolation<?> constraintViolation;

    private final List<String> pathes;

    private final List<String> labels;

    private int deepIndex;

    private LabelConverter(ConstraintViolation<?> constraintViolation, List<String> paths) {
        this.constraintViolation = constraintViolation;
        this.pathes = paths;
        this.labels = new ArrayList<>();
        this.deepIndex = 0;
    }

    public static LabelConverter of(ConstraintViolation<?> constraintViolation) {
        List<String> pathes = Arrays.asList(constraintViolation.getPropertyPath().toString().split("\\."));
        return new LabelConverter(constraintViolation, pathes);
    }

    public String filter() {
        Class<?> clazz = this.constraintViolation.getRootBeanClass();
        this.recursiveFilter(clazz, this.pathes.get(deepIndex));

        Class<?> invalidClazz = constraintViolation.getInvalidValue().getClass();
        Label annotation = invalidClazz.getAnnotation(Label.class);
        if (annotation != null) {
            this.labels.add(annotation.value());
        }

        //インターフェースからキー値を編集
        if (FormObject.class.isAssignableFrom(invalidClazz)) {
            this.labels.add(invalidClazz.getCanonicalName() + ".label");
        }

        String _label = this.labels.isEmpty() ? "" : this.labels.get(0);
        return _label;

    }

    void recursiveFilter(Class<?> clazz, String property) {

        try {
            Label classLabel = clazz.getAnnotation(Label.class);
            if (classLabel != null) {
                this.labels.add(classLabel.value());
            }

            //インターフェースからキー値を編集
            if (FormObject.class.isAssignableFrom(clazz)) {
                this.labels.add(clazz.getCanonicalName() + ".label");
            }

            Field field = clazz.getDeclaredField(property);
            Label fieldLabel = field.getAnnotation(Label.class);
            if (fieldLabel != null) {
                this.labels.add(fieldLabel.value());
            }

            Class<?> nextClass = clazz.getDeclaredField(property).getType();

            if (deepIndex < this.pathes.size() - 1) {
                deepIndex++;
                this.recursiveFilter(nextClass, this.pathes.get(deepIndex));
            }
        } catch (NoSuchFieldException | SecurityException ex) {
        }

    }

}

Propertyファイル

  • 参照するPropertyは専用のものを準備

  • キー値は「クラスパス」+「label」として衝突を避ける

  • キー値は「Label」で指定したプロパティーキー

というようなルールを設けました。

org.vermeerlab.targets.form.nolabel.FormItemNoLabel.label = 【Formクラスには設定がない】
FormWithPropertyKey.label = 【直接指定のプロパティキー】
FormItemWithPropertyKey.label = 【Form指定のプロパティキー】

実行結果

(略)

Code

Bitbucket

さいごに

今回はコード貼り付けだけという感じですが、イイ感じ(?)に混沌としてきました。

とりあえず、やりたいことは盛り込めているように思います。

ただ、こういうEasyに向けた仕組みは黒魔術になるんですよね。

そうなると、ルールをドキュメントにするのも大事だけれど、規約以外で(FindBugsみたいな感じで)順守できるような仕組みも同時に作りこまないと陳腐化しそうなんですよね。

これは、また別で考えよう。

FormへのBeanValidationにラベルをつける

はじめに

標準のBeanValidationで宣言的に実装する というのをするのは良いのですが、表示させるメッセージとしては「どの項目」というラベルが欲しいところです。

messageに項目名を付与した文言を都度指定する、というやり方もあると思いますが、テンプレートを使えるというメリットが無くなってしまいます。

使用側

FormでLabel指定(Formで検証不正)

定義側

@Label("Form Class Label")
public class LabelFormItem implements FormObject<String> {

(略)

}

結果

<<< FormValidationのメッセージにラベルが使用されます(FormObjecrt内指定) >>>
設定した値                               = 
検証時に変換されたメッセージ = {org.vermeerlab.validation.FormNotBlank.message}
遅延変換したメッセージ           = Form Class Labelは入力必須です。

FormでLabel指定(ValueObjectで検証不正)

定義側

FormでLabel指定(Formで検証不正) と同じ。

結果

<<< ValueValidationのメッセージにラベルが使用されます(FormObjecrt内指定) >>>
設定した値                               = 2
検証時に変換されたメッセージ = must be greater than or equal to 100
遅延変換したメッセージ           = Form Class Labelは100以上にしてください。

Formの使用側でLabelを指定(Formで検証不正)

定義側

@Data
public class LabelFormDirectLabel {

    @Valid
    @Label("Direct  Form Label")
    @FormNotBlank(groups = FormValidation.class)
    private LabelFormItem item;

}

結果

<<< FormValidationのメッセージにラベルが使用されます(直接指定) >>>
設定した値                               = 
検証時に変換されたメッセージ = {org.vermeerlab.validation.FormNotBlank.message}
遅延変換したメッセージ           = Direct  Form Labelは入力必須です。

LabelFormItemのクラスにもLabelを指定していますが、直接指定したものが優先されます。

Formの使用側でLabelを指定(ValueObjectで検証不正)

定義側

Formの使用側でLabelを指定(Formで検証不正) と同じ。

結果

<<< ValueValidationのメッセージにラベルが使用されます(直接指定) >>>
設定した値                               = 2
検証時に変換されたメッセージ = must be greater than or equal to 100
遅延変換したメッセージ           = Direct  Form Labelは100以上にしてください。

実装

Labelの読み込み

優先度は

Formで自分のラベルを持つ < Formの使用側でラベルを指定する

としました。

ざっくりいうと、外側で指定した情報を優先して上書きするというイメージです。

ポイントの抜粋

LabelAnnotationFilterでLabel情報を検証結果から取得しています。

List<String> pathes = Arrays.asList(constraintViolation.getPropertyPath().toString().split("\\."));

で検証でプロパティ名の数珠繋ぎを取得できるので、それをListにして再帰で順番に情報を取得しています。


Label classLabel = clazz.getAnnotation(Label.class);

でクラスに付与したLabelを取得

Field field = clazz.getDeclaredField(property);
Label fieldLabel = field.getAnnotation(Label.class);

でフィールドのLabelを取得

Label annotation = constraintViolation.getInvalidValue().getClass().getAnnotation(Label.class);

で検証不正の原因となったFormそのもののLabelを取得

Listで情報を保有せず、そのまま値を上書きしても良いところなのですが、階層を考慮したLabelの出力をしたいかもというボンヤリした構想もあったりしてListにしました。

Code

Bitbucket

さいごに

リフレクションを使ってどうにかして なんとかここまで という感じです。

リフレクションを使ったやり方は好ましくないのですが他に方法が思いつきませんでした。

しかも、これでラベルの問題は解決したかというと、そうでもないですね。

ラベル自体のプロパティ化をしておかないと結局のところ中途半端ですね。。

具体的には国際化対応のところで、直接入力した値(ここだと日本語)について、何かしら置き換えられる仕組みを設けておかないといけないと思っています。

あと、なんとなく、階層が複雑なバリエーションの場合、想定していないLabelを適用しそうな気もしています。

いずれにしても、まだBeanValidation関連で試してみたいことは残っているので、それらが一通り終わったら、改めてオレオレライブラリにするべく整理とテストをするつもりです。*1

*1:予定は未定ですが

FormとValueObjectの関係を整理する(続)

前回はコードのイメージということで全量を公開していませんでした。

vermeer.hatenablog.jp

今回は、前回はしていない部分のコードの説明を中心にしていきたいと思います。

Form用のValidation関連

マーカーとなるアノテーションと優先度を示すアノテーションで、Formのアノテーションの検証でエラーになったら後続検証はしないようにしています。

やってみて、残念に思ったのはClass<?>[] groups() default {};FormValidation.classを指定できなかったこと。

これが出来たらForm側の検証アノテーションの記述をもっとスッキリできたのに。。

独自アノテーション

@Target({METHOD, FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {FormNotBlankValidator.class})
public @interface FormNotBlank {

    String message() default "{org.vermeerlab.validation.FormNotBlank.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({METHOD, FIELD})
    @Retention(RUNTIME)
    @Documented
    @interface List {

        FormNotBlank[] value();
    }
}

独自アノテーションの実装

public class FormNotBlankValidator implements ConstraintValidator<FormNotBlank, FormObject<String>> {

    @Override
    public void initialize(FormNotBlank constraintAnnotation) {

    }

    @Override
    public boolean isValid(FormObject<String> value, ConstraintValidatorContext context) {
        return Objects.equals(value.getValidatationValue(), "") == false;
    }
}

検証順の指定

@GroupSequence({FormValidation.class})
public @interface ValidationPriority {
}
public interface FormValidation {
}

検証用情報を付与するためのインターフェース

Validationを実行する際、Formの検証したい値を返却するためのインターフェースです。

Webは物理的に「文字列」だと思っているので、String固定でも良いかなぁと思ったのですが、検証用の戻り値の型は任意に指定できるようにしておきました。

出力情報なので無理にキャストを強制させる必然もないだろうという考えと、数値としてBeanValidationを行いたいときに面倒なことになりそうな気がした、というところです。

public interface FormObject<ReturnType> {

    /**
     * Formを検証に使用する値を返却します.
     * <P>
     * BeanValidationで使用する値はプリミティブ型である必要があり、ValueObjectにはAnnotateできません。
     * したがって、本メソッドを使用して内部のプリミティブ値を返却します.
     *
     * @return BeanValidationに使用する値
     */
    public ReturnType getValidatationValue();
}

テストクラス(抜粋)

@Test
public void formItemIsBlankWithFormValidation() {
    Form form = new Form();
    FormItem item = new FormItem("");
    form.setItem(item);

    Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    Set<ConstraintViolation<Form>> results = validator.validate(form, FormValidation.class);

    System.out.println("<<< FormValidationを優先的に実行するので、ValueObjectのValidationは実装されません >>>");

    for (ConstraintViolation<Form> result : results) {
        BeanValidationMessageInterpolator interpolator = BeanValidationMessageInterpolator.create();
        String convertedMessage = interpolator.toMessage(result);

        System.out.println("設定した値                               = " + result.getInvalidValue());
        System.out.println("検証時に変換されたメッセージ = " + result.getMessage());
        System.out.println("遅延変換したメッセージ           = " + convertedMessage);
        System.out.println();
    }

}

@Test
public void formItemValidateValueObject() {
    Form form = new Form();
    FormItem item = new FormItem("1");
    form.setItem(item);

    Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    Set<ConstraintViolation<Form>> results = validator.validate(form);

    System.out.println("<<< FormValidationはエラーにならないので、ValueObjectの検証が実行されます >>>");

    for (ConstraintViolation<Form> result : results) {
        BeanValidationMessageInterpolator interpolator = BeanValidationMessageInterpolator.create();
        String convertedMessage = interpolator.toMessage(result);

        System.out.println("設定した値                               = " + result.getInvalidValue());
        System.out.println("検証時に変換されたメッセージ = " + result.getMessage());
        System.out.println("遅延変換したメッセージ           = " + convertedMessage);
        System.out.println();
    }

}

実際のコードとしては、FormValidationを先に実行して、エラーがなければ引数無しのvalidationを改めて実行するイメージです。

Code

Bitbucket

さいごに

ベタに実装するとForm用のvalidationと、全てのvalidationの2回実施になってしまうので どうも冗長です。

標準の仕組みから逸脱しない(やりすぎない)方式だと、私がざっくり試した感じのアイディアとしては こんな感じです。

validatorについては個人的にこうあってほしいという思いもあるので、結局「標準ライブラリをラップしたオレオレライブラリ」を作ってしまう気がします。

Easyのやりすぎは良くないと思っているのですが、面倒くさがりやなので 結局 何かしらを作りたくなるんですよね。。

かといって、すべて自作する気も無くて「イイ感じに標準仕様と参照実装を活用する」のが私の限界という感じです。