cypher256's blog

Pleiades とか作った

@Reset アノテーション

フォームをセッションで持つと、検索画面や変更画面にあるチェックボックスなどの値は画面でチェックをはずすと、前の値が残ってしまうので、リクエストの値がフォームにセットされる前にクリアする必要があります。SAStruts にも Struts と同じようにフォームに reset メソッドを作成することで回避可能ですが、1 機能 1 アクションにすると、すべてのアクションメソッドでリセットされてしまいます。

@Reset アノテーションを作ることにより、アクションメソッドごとにリセットメソッドを指定できます。こんな感じ。

// リセット・アノテーション
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Reset {
    String method() default "reset";
}

今回、属性名を method としましたが、アノテーション仕様により、属性が 1 つの場合は属性名を value とすることで、使うときに @Reset("resetSearch") のように method = のような記述を省略することができます。ただ、この場合、属性を増やすときに使用部分も修正が必要になってしまうので注意が必要です。

// アクション・クラス
public class HogeAction {
    // 検索
    @Execute(・・・)
    @Reset(method = "resetSearch")
    public String search() {
    }
    // 変更画面表示
    @Execute(・・・)
    @Reset(method = "resetDetail")
    public String detail() {
    }
}

最初はフォーム・クラスのリセット・メソッドに @Reset アノテーションを付けて、属性でアクション・メソッドを指定しようと思ったのですが、なんかしっくりこないので、アクションの実行メソッドに付けたほうが良いと思います。

// フォーム・クラス
@Component(instance = InstanceType.SESSION)
public class HogeDto implements Serializable {
    // 検索時のリセット
    public void resetSearch() {
        xxx = null;
    }
    // 変更画面表示時のリセット
    public void resetDetail() {
        yyy = null;
    }
}


あとは、@Reset アノテーションを処理するためにリクエスト・プロセッサの processPopulate メソッドを拡張します。SAStruts でも Struts でもこのメソッドは reset メソッドがあれば、それを呼び出し、その後リクエストをフォームにコピーします。そこで、ActionFormWrapper (SAStruts でのフォームの実体) を無名インナークラスで拡張し、reset メソッドの中で @Reset アノテーションで指定された Dto のリセットメソッドを呼び出すようにしておきます。この無名クラスのインスタンスを引数にして super で親の processPopulate メソッドを呼び出せば、無名クラスの reset が呼び出され、@Reset で指定したのメソッドが呼び出されることになります。

// リクエスト・プロセッサの拡張クラス
public class HogeRequestProcessor extends S2RequestProcessor {

    @Override
    protected void processPopulate(HttpServletRequest request,
            HttpServletResponse response, ActionForm form, ActionMapping mapping)
            throws ServletException {

        if (form == null) {
            return;
        }
        S2ExecuteConfig executeConfig = ((S2ActionMapping) mapping)
            .findExecuteConfig(request);

        if (executeConfig != null) {

            Method actionMethod = executeConfig.getMethod();
            Reset resetAnno = actionMethod.getAnnotation(Reset.class);
            final String resetMethodName = (resetAnno == null) ? null
                    : resetAnno.method();

            ActionFormWrapper formWrapper = (ActionFormWrapper) form;
            ActionFormWrapperClass formWrapperClass = (ActionFormWrapperClass) formWrapper
                .getDynaClass();

            form = new ActionFormWrapper(formWrapperClass) {
                @Override
                public void reset(ActionMapping mapping,
                        HttpServletRequest request) {
                    if (resetMethodName == null) {
                        return;
                    }
                    try {
                        Method resetMethod = actionForm.getClass().getMethod(
                            resetMethodName);
                        MethodUtil.invoke(resetMethod, actionForm, null);
                    } catch (NoSuchMethodException e) {
                        NoSuchMethodException ne = new NoSuchMethodException(
                            "@Reset アノテーションの属性 method に指定されたメソッドが見つかりません。" + e);
                        throw new NoSuchMethodRuntimeException(actionForm
                            .getClass(), resetMethodName, null, ne);
                    }
                }
            };
        }
        super.processPopulate(request, response, form, mapping);
    }
}

検証用アノテーション

SAStruts では Struts 標準の入力チェックに対応するアノテーションが用意されていて、Mask を使えば大概のことはできますが、それだけだと実際の業務プロジェクトでは開発者が苦労することになります。小数点を含む数値に @DoubleType とか使うと "1d" とか "1f" が通っちゃいますからね。

そこで検証用アノテーションを作ることになります。こんな。

// 数値チェック・アノテーション
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Validator("numberCheck")
public @interface NumberCheck {
    /** 数値最大桁数 (数字全体の最大有効桁数 例)123.4 -> 4) */
    int maxlength();
    /** 小数部桁数 */
    int scale() default 0;
    /** 最小値 */
    double min() default 0;
    /** 最大値 */
    double max() default Double.MAX_VALUE;
    /** 検証の対象となるメソッド名を指定 (複数ある場合はカンマ区切り) */
    String target() default "";
}
// フォーム・クラス
@Component(instance = InstanceType.SESSION)
public class HogeDto implements Serializable {
    @NumberCheck(maxlength = 5)
    public String xxx;
    @NumberCheck(maxlength = 5, scale = 2)
    public String yyy;
    @NumberCheck(maxlength = 5, scale = 2, min = -100, max = 100)
    public String zzz;
}


チェックの実装は Struts の FieldChecks や SAStruts の S2FieldChecks が参考になりますが、上記のようなアノテーションの場合、チェック結果によりメッセージを切り替えなければなりません。例えば、不正フォーマット時、整数桁数エラー、小数部桁数エラー、全体桁数エラー、範囲エラーなどがあります。参考となる FieldChecks は 1 つのメッセージしか対応していないため、メッセージを可変にする場合は、下記のように commons validator のメッセージを自分で生成する必要があります。

public class HogeFieldCheckes implements Message {

    // @NumberCheck に対応したチェック
    public static boolean validateNumber(Object bean, ValidatorAction va,
            Field field, ActionMessages errors, Validator validator,
            HttpServletRequest request) {

        // エラー・コンテキストの生成
        ErrorsContext context = new ErrorsContext(
            va,
            field,
            errors,
            validator,
            request);
        try {
            String value = ValidatorUtils.getValueAsString(bean, field
                .getProperty());

            if (GenericValidator.isBlankOrNull(value)) {
                return true;
            }
            // 数値チェック
            if (数値エラー) {
                context.add(ERRORS_COMMON_NUMBER);
                return false;
            }
            // 桁数チェック
            Double doubleValue = Double.parseDouble(value);
            Integer length = Integer.parseInt(field.getVarValue("maxlength"));
            Integer scale = Integer.parseInt(field.getVarValue("scale"));
            if (整数部桁数エラー) {
                context.add(ERRORS_COMMON_NUMBER_INTEGER);
                return false;
            }
            if (全体桁数エラー) {
                context.add(ERRORS_COMMON_LENGTH);
                return false;
            }
            if (小数部桁数エラー) {
                context.add(ERRORS_COMMON_NUMBER_SCALE);
                return false;
            }
            // 範囲チェック
            Double min = Double.parseDouble(field.getVarValue("min"));
            Double max = Double.parseDouble(field.getVarValue("max"));
            if (範囲エラー) {
                context.add(ERRORS_COMMON_NUMBER_RANGE);
                return false;
            }
        } catch (Exception e) {
            context.add(ERRORS_COMMON_NUMBER);
            return false;
        }
        return true;
    }

上記で生成しているエラー・コンテキストは下記のような commons validator のメッセージを作成するためのクラスです。

    // エラー・コンテキスト・クラス
    private static class ErrorsContext {

        private final ValidatorAction va;
        private final Field field;
        private final ActionMessages errors;
        private final Validator validator;
        private final HttpServletRequest request;

        // コンストラクタ
        public ErrorsContext(ValidatorAction va, Field field,
                ActionMessages errors, Validator validator,
                HttpServletRequest request) {
            this.va = va;
            this.field = field;
            this.errors = errors;
            this.validator = validator;
            this.request = request;
        }

        // エラー追加
        public void add(String key, Object... args) {
            Msg m = new Msg();
            m.setName(field.getDepends());
            m.setKey(key);
            m.setResource(true);
            field.addMsg(m);
            for (int i = 0; i < args.length; i++) {
                Arg a = new Arg();
                a.setKey(String.valueOf(args[i]));
                a.setResource(false);
                a.setPosition(i);
                field.addArg(a);
            }
            errors.add(field.getKey(), Resources.getActionMessage(
                validator,
                request,
                va,
                field));
        }
    }

ネストしたプロパティで入力チェックの実装

2008-04-19 のエントリー。

自分でカスタマイズするなら ActionCustomizer を継承し、setupValidator メソッドに少しコードを追加するすることで対応可能です。プロパティを getClass() して、その定義クラスを getFileds() し、getAnnotations() した後、プロパティ名 + "." + ネストプロパティ名をバリデーション対象として登録みたいな感じだったと思います。

ネストしたプロパティで入力チェック - C/pHeR Memo - Java とか。Eclipse とか。

コード・イメージはこんなのです。

// アクション・カスタマイザ・クラス
public class HogeActionCustomizer extends ActionCustomizer {

    // バリデータのセットアップ
    @Override
    protected void setupValidator(S2ActionMapping actionMapping,
            S2ValidatorResources validatorResources) {
        ・・・(省略)
        for (int i = 0; i < beanDesc.getPropertyDescSize(); i++) {
            ・・・(省略)
            for (Annotation anno : field.getDeclaredAnnotations()) {
                processAnnotation(
                    pd.getPropertyName(),
                    anno,
                    validatorResources,
                    forms);
            }
            // 追加 ここから
            Class<?> c = field.getType();
            if (c != String.class && c != Boolean.class) {
                for (Field f : c.getFields()) {
                    for (Annotation anno : f.getDeclaredAnnotations()) {
                        processAnnotation(pd.getPropertyName() + "."
                                + f.getName(), anno, validatorResources, forms);
                    }
                }
            }
            // 追加 ここまで
        }
        validatorResources.addForm(baseForm);
        for (Iterator<Form> i = forms.values().iterator(); i.hasNext();) {
            validatorResources.addForm(i.next());
        }
    }
}

フレームワーク拡張問題

業務に依存しない部分で、かつフレームワークを作った方のポリシーに合うものなら、できるだけフレームワークを作った方に対応していただくことがベストなやり方です。拡張するときにすべてを把握できていないため、見えない問題が存在していたり、バージョンアップしたときに動かなくなる可能性があります。

今回、開発期間も短いこともあり、取り急ぎ、色々拡張しましたが、先日の SAStruts のバージョンアップで上記の ネストしたプロパティで入力チェック が動かなくなっていました(上記のコードは最新のものに対応済みのものです)。拡張した本人がいる間は良いのですが、いなくなった後のメンテで問題が発生するものあれですからね。

数年前、ユーザ数が数十万人のプロジェクトでお客様の要望により TomcatSSL 動作を作り変えました。Servlet 非標準な動作です。今、大丈夫かなー。Tomcat バージョンアップして死んでないかなー。