Home > Java

Java Archive

Cubbyのアクションクラスで定数を利用してリファクタリングしやすくする

Cubbyのアクションクラスにはバリデーションルールの定義時やアノテーションに文字列が登場します。文字列で指定しているのでリファクタリングする時に修正漏れなどがおきそうでちょっと怖いです。そこで、それらの文字列を定数にしてしまえば、少しだけリファクタリングしやすくなりそうです。また、IDE上での参照元・参照先への移動や使われているかどうかのチェックも簡単になります。

例えば、次のようなアクションクラスがあるとします。

@Path("hoge")
public class HogeAction extends AbstractAction {

    @Binding(bindingType = BindingType.NONE)
    protected ValidationRules processApplyValidation =
        new DefaultValidationRules() {
            @Override
            public void initialize() {
                add("token", new TokenValidator());
                add("name", new TokenValidator());
                add("comment", new TokenValidator());
            }
        };

    @RequestParameter
    public HogeParameterDto hogeParameterDto;

    @Path("process")
    @Accept(POST)
    @OnSubmit("apply")
    @Form("hogeParameterDto")
    @Validation(rules = "processApplyValidation", errorPage = "/hoge/edit.html")
    public ActionResult processApply() {
        return new Forward("/hoge/edit.html");
    }

}

これを次のようにします。

import static org.example.test.entity.names.Names.*;

@Path("hoge")
public class HogeAction extends AbstractAction {

    private static final String processApplyValidationName
        = "processApplyValidation";
    @Binding(bindingType = BindingType.NONE)
    protected ValidationRules processApplyValidation =
        new DefaultValidationRules() {
            @Override
            public void initialize() {
                add(TOKEN, new TokenValidator());
                add(post().name().toString(), new TokenValidator());
                add(post().comment().toString(), new TokenValidator());
            }
        };

    private static final String hogeParameterDtoName = "hogeParameterDto";
    @RequestParameter
    public HogeParameterDto hogeParameterDto;

    @Path("process")
    @Accept(POST)
    @OnSubmit("apply")
    @Form(hogeParameterDtoName)
    @Validation(rules = processApplyValidationName, errorPage = "/hoge/edit.html")
    public ActionResult processApply() {
        return new Forward("/hoge/edit.html");
    }

}

変更した箇所を説明します。

全文を読む

Cubbyでアクションメソッドに指定するアノテーションの順番

しばらく時間が空いてからCubbyを使ったWEBアプリケーションを書く時に、アクションメソッドに指定するアノテーションの順番どうだったかなっと考えるのでまとめます。アノテーションなので適当な順番に書いても問題なく動作しますが、処理の流れを考えて次の順番で指定しています。

  1. URIを指定: @Path
  2. HTTPメソッドを指定: @Accept
  3. 同一URIへのPOST時などに実行するアクションメソッドを変える指定: @OnSubmit
  4. リクエストパラメータのバインド先を指定: @Form
  5. リクエストパラメータのバリデーションを指定: @Validation
@Path("hoge")
public class HogeAction extends AbstractAction {
    ...
    @Path("process")
    @Accept(POST)
    @OnSubmit("apply")
    @Form("hogeParameterDto")
    @Validation(rules = "processApplyValidation", errorPage = "/hoge/edit.html")
    public ActionResult processApply() {
        return new Forward("/hoge/edit.html");
    }
    ...
}

JIRA 4.xと新しいPlugin機構

Seasarプロジェクトでも利用しているJIRA のライセンスを去年の10月に自分用に購入しました。JIRAを開発しているAtlassian社が10ユーザまで利用できるGet Started for $10というライセンスを始めたので思わず買いました。JIRA 4.0になって、JIRA 3.x以前にあったエディションの違いが無くなり、$10でEnterpriseエディションが使えるというのは破格です。JIRA 3.xまではLDAPサポートにEnterprise以上が必要でとても買える金額では無かったため嬉しいです。また、このライセンスの購入費用は全額 Room to Read に寄付されるプログラムになっています。

日本円で購入したい場合は、Atlassian社のOfficial Atlassian Partnerである リックソフト株式会社 が1,050 円で販売されています。

そんなわけで、だいぶ前置置きが長くなりましたが、自分用にJIRAの環境を作りました。購入当初JIRA 4.0には 新しく導入されたガジェットの仕組みがSSL環境で上手く動かない問題 があったので、使い始めたのはこの問題がJIRA側で解決した4.0.1からです。

ただ、まだ問題があって、Seasarプロジェクトで利用していた3.xのインストール手順でwarファイルを作成し、Tomcatにデプロイするとエラーがでて上手く起動しないのです。いろいろ試してみると自分で追加していたPluginを全部外すと起動しました。そんなことをつぶやいていたらリックソフトのohnukiさんがコンタクトをくださっていろいろやりとりした結果、原因が判明しました。

原因は、JIRA 4.0からOSGiを使用した新しいPlugin機構(Version 2)がサポートされ、JIRA 3.xの時とPluginの配置の仕方が変わっていたことでした。これに気付かず、JIRA 3.xの導入の仕方で入れていたので上手くPluginがロードされなかったようです。一応、互換性のため、古いPluginの配置方法(Version 1)も使えるようですが、Tomcat 5.5.28では動作するものの、Tomcat 6.0.20では動作せず、それでエラーが出ていました。

  • 参考: Managing JIRA’s Plugins
    • Version 1: atlassian-jira/WEB-INF/lib/ 以下に配置
    • Version 2: jira.home/plugins/installed-plugins/ 以下に配置
      • jira.home はJIRA 4.0から導入されたホームディレクトリ指定

というわけで、無事やっとPluginも使えるようになりました。ohnukiさんありがとうございました!

Pluginが使えるようになったので、SeasarプロジェクトのJIRAも近日アップグレードのテストをし、アナウンスを出した後に現在の3.13.5から4.0.1に更新したいと思います。

以下、何かの参考までにVersion 1 + Tomcat 6.0.20で起動した時の例外全文を記載しておきます。

全文を読む

入力された文字の前後の空白スペースなどの文字列を除去しても有効な文字があるか検証するValidatorと実際に取り除く処理

入力フォームでユーザに文字を入力させると、前後に空白スペースなどを入れちゃったりしますよね。Validatorで前後に空白スペースがあればエラーにしても良いですが、それはちょっと不親切なので自動的に取り除いてあげるのが親切かなと思います。また、String の trim() 関数だと全角スペース1文字がエラーになりません。得てして前後にスペースを入れちゃう人は全角のスペースを入れるのでこれもチェックしないといけません。というわけで、Cubby 1.1.x 向けにValidatorを書いてみました。

取り除きたい文字は半角・全角スペースだけとも限らないので、取り除きたい正規表現を前後別々に指定できるようになっています。引数が1個の時は前後とも同じ正規表現が使用されます。

  • RegexTrimRequiredValidator
import org.seasar.cubby.validator.MessageHelper;
import org.seasar.cubby.validator.ScalarFieldValidator;
import org.seasar.cubby.validator.ValidationContext;
import org.seasar.framework.util.StringUtil;

/**
 * 先頭と末尾を正規表現でtrimした文字列の必須検証をします。
 * <p>
 * trimした文字列の長さが0の場合、検証エラーとなります。
 * </p>
 * <p>
 * デフォルトエラーメッセージキー: valid.required
 * </p>
 *
 * @author jfut
 */
public class RegexTrimRequiredValidator implements ScalarFieldValidator {

    /** メッセージヘルパ */
    private final MessageHelper messageHelper;
    /** 文字の先頭から除去する正規表現 */
    private String prefixRegexTrim;
    /** 文字の末尾から除去する正規表現 */
    private String postfixRegexTrim;

    /** 正規表現における先頭識別文字 */
    private static final char BEGIN_CHAR = '^';
    /** 正規表現における末尾識別文字 */
    private static final char END_CHAR = '$';

    /**
     * インスタンスを作成します。
     */
    public RegexTrimRequiredValidator() {
        this(null, null);
    }

    /**
     * インスタンスを作成します。
     *
     * @param regexTrim
     *            除去する正規表現
     */
    public RegexTrimRequiredValidator(final String regexTrim) {
        this(regexTrim, regexTrim);
    }

    /**
     * インスタンスを作成します。
     *
     * @param prefixRegexTrim
     *            文字の先頭から除去する正規表現
     * @param postfixRegexTrim
     *            文字の末尾から除去する正規表現
     */
    public RegexTrimRequiredValidator(final String prefixRegexTrim,
            final String postfixRegexTrim) {
        this(prefixRegexTrim, postfixRegexTrim, "valid.required");
    }

    /**
     * エラーメッセージキーを指定してインスタンスを作成します。
     *
     * @param prefixRegexTrim
     *            文字の先頭から除去する正規表現
     * @param postfixRegexTrim
     *            文字の末尾から除去する正規表現
     * @param messageKey
     *            エラーメッセージキー
     */
    public RegexTrimRequiredValidator(final String prefixRegexTrim,
            final String postfixRegexTrim, final String messageKey) {
        this.prefixRegexTrim = prefixRegexTrim;
        this.postfixRegexTrim = postfixRegexTrim;
        this.messageHelper = new MessageHelper(messageKey);
        setupRegexTrim();
    }

    /**
     * 正規表現をセットアップします。
     */
    public void setupRegexTrim() {
        // prefixRegexTrim
        if (prefixRegexTrim != null) {
            // 文字の末尾が $ の場合、取り除きます
            if (prefixRegexTrim.charAt(prefixRegexTrim.length() - 1) == END_CHAR) {
                prefixRegexTrim =
                    prefixRegexTrim.substring(0, prefixRegexTrim.length() - 1);
            }
            // 文字の先頭に ^ が無い場合、追加します
            if (prefixRegexTrim.charAt(0) != BEGIN_CHAR) {
                prefixRegexTrim = BEGIN_CHAR + prefixRegexTrim;
            }
        } else {
            prefixRegexTrim = "";
        }
        // postfixRegexTrim
        if (postfixRegexTrim != null) {
            // 文字の先頭が ^ の場合、取り除きます
            if (postfixRegexTrim.charAt(0) == BEGIN_CHAR) {
                postfixRegexTrim =
                    postfixRegexTrim.substring(1, postfixRegexTrim.length());
            }
            // 文字の末尾に $ が無い場合、追加します
            if (postfixRegexTrim.charAt(postfixRegexTrim.length() - 1) != END_CHAR) {
                postfixRegexTrim = postfixRegexTrim + END_CHAR;
            }
        } else {
            postfixRegexTrim = "";
        }
    }

    /**
     * {@inheritDoc}
     */
    public void validate(final ValidationContext context, final Object value) {
        if (value instanceof String) {
            final String str = trim((String)value);
            if (!StringUtil.isEmpty(str)) {
                return;
            }
        } else if (value != null) {
            return;
        }
        context.addMessageInfo(this.messageHelper.createMessageInfo());
    }

    /**
     * 指定された文字列の先頭と末尾をtrimします。
     *
     * @param value
     *            文字列
     * @return trimされた文字列
     */
    public String trim(String value) {
        if (value != null) {
            value = value.replaceAll(prefixRegexTrim, "");
            value = value.replaceAll(postfixRegexTrim, "");
        }
        return value;
    }
}
  • 適当なActionでの使用例

入力値をチェックしつつ、不要な文字を取り除いても有効な文字がある時は、同じ条件で不要な文字をtrimできるようにインスタンス化して使用します。ポイントはパラメータがバインドされたインスタンスをtrimするために、ValidationRulesでValidateしつつ、最後にインナークラスRegexTrimValidationRuleFilterでtrimを実行しておくところです。これによりアクションメソッド実行時には既にtrimされた値が入ったインスタンスを使うことができます(参考: [リクエストからアクション実行までのフロー|http://cubby.seasar.org/action.html#リクエストからアクション実行までのフロー]、Action#preactionなんてのがあるとそこが適切かも?Interceptorだとちょっと書きにくいし)。

この例では、入力値の前後の半角・全角スペースをすべて取り除いても有効な文字があるかどうかをチェックし、そして、RegexTrimValidationRuleFilterで実際に不要な文字を取り除きます。

@Path("register")
public class RegisterAction extends Action {

    // -------------------------------------------------- [Validation]

    private final RegexTrimRequiredValidator trimRequiredValidator =
        new RegexTrimRequiredValidator("[\\s ]*");

    @Binding(bindingType = BindingType.NONE)
    public ValidationRules registerValidation = new DefaultValidationRules() {
        @Override
        public void initialize() {
            add("token", new TokenValidator());
            add("lastName", trimRequiredValidator);
            add("firstName", trimRequiredValidator);
            add("lastNameEnglish", trimRequiredValidator);
            add("firstNameEnglish", trimRequiredValidator);
            add("mailAddress1", trimRequiredValidator, new EmailValidator());
            add("mailAddress2", trimRequiredValidator, new EmailValidator());
            add("inside", new RequiredValidator());
            // add("uid", validator);
            add("laboratory", trimRequiredValidator);
            add("promotion", trimRequiredValidator);
            add(...他のValidationRule...);
            add(new RegexTrimValidationRuleFilter());
        }
    };

    // -------------------------------------------------- [DI Filed]

    @Resource
    private UserService userService;

    // -------------------------------------------------- [Attribute]

    protected RegisterFormDto registerFormDto;

    // -------------------------------------------------- [Action Method]

    public ActionResult index() {
        return new Forward("/register/index.html");
    }

    @Accept(POST)
    @Form("registerFormDto")
    @Validation(rules = "registerValidation", errorPage = "/register/index.html")
    public ActionResult confirm() {
        return new Forward("/register/confirm.html");
    }

    @Path("process")
    @OnSubmit("apply")
    @Accept(POST)
    @Form("registerFormDto")
    @Validation(rules = "registerValidation", errorPage = "/register/confirm.html")
    public ActionResult processApply() {
        registerFormDto.mailAddress = registerFormDto.mailAddress1;
        User user =
            Beans.createAndCopy(User.class, registerFormDto).execute();
        userService.insertAndSendMail(user);
        return new Forward("/register/success.html");
    }

    ... 他のアクションメソッド省略 ...

    // -------------------------------------------------- [Helper Method]

    // -------------------------------------------------- [Validation Class]

    private class RegexTrimValidationRuleFilter implements ValidationRule {
        public void apply(Map<String, Object[]> params, Object form,
                ActionErrors errors) {
            // BeanDesc と PropertyDesc を使って汎用的にしても良いですね
            registerFormDto.lastName =
                trimRequiredValidator.trim(registerFormDto.lastName);
            registerFormDto.firstName =
                trimRequiredValidator.trim(registerFormDto.firstName);
            registerFormDto.lastNameEnglish =
                trimRequiredValidator.trim(registerFormDto.lastNameEnglish);
            registerFormDto.firstNameEnglish =
                trimRequiredValidator.trim(registerFormDto.firstNameEnglish);
            registerFormDto.mailAddress1 =
                trimRequiredValidator.trim(registerFormDto.mailAddress1);
            registerFormDto.mailAddress2 =
                trimRequiredValidator.trim(registerFormDto.mailAddress2);
            registerFormDto.laboratory =
                trimRequiredValidator.trim(registerFormDto.laboratory);
            registerFormDto.promotion =
                trimRequiredValidator.trim(registerFormDto.promotion);
        }
    }

    ...
}
  • RegexTrimRequiredValidatorTest

テストケースも書いておきます(半角・全角スペースの違いが判り難いかも)。

public class RegexTrimRequiredValidatorTest {
    @Test
    public void test1() {
        RegexTrimRequiredValidator validator;
        validator = new RegexTrimRequiredValidator();
        assertNull(validator.trim(null));
        assertEquals("", validator.trim(""));
        assertEquals(" ", validator.trim(" "));
        assertEquals("abc", validator.trim("abc"));

        validator = new RegexTrimRequiredValidator("[\\s ]*");
        assertNull(validator.trim(null));
        assertEquals("", validator.trim(""));
        assertEquals("", validator.trim(" "));
        assertEquals("", validator.trim(" "));
        assertEquals("a b", validator.trim(" a b "));
        assertEquals("a b", validator.trim("   a b   "));
        assertEquals("abc", validator.trim("abc"));

        validator = new RegexTrimRequiredValidator("^[\\s ]*$");
        assertNull(validator.trim(null));
        assertEquals("", validator.trim(""));
        assertEquals("", validator.trim(" "));
        assertEquals("", validator.trim(" "));
        assertEquals("a b", validator.trim(" a b "));
        assertEquals("a b", validator.trim("   a b   "));
        assertEquals("abc", validator.trim("abc"));

        validator = new RegexTrimRequiredValidator("^[\\s ]*$", "^[\\s]*$");
        assertNull(validator.trim(null));
        assertEquals("", validator.trim(""));
        assertEquals("", validator.trim(" "));
        assertEquals("", validator.trim(" "));
        assertEquals("a b", validator.trim(" a b "));
        assertEquals("a b   ", validator.trim("   a b   "));
        assertEquals("abc", validator.trim("abc"));
    }
}

書いてみてなかなか便利だったので3月に書いたアプリでは大活躍でした(^^)。

[2009-04-17 14:09追記]: 日記用にActionクラスを適当に書き得てたとこがおかしかったので修正。

仮型引数を持つ関数を型引数を持つ関数でOverrideしたクラスからその関数をリフレクションで取得するとそれぞれ別の関数として見つかる

Buri + S2JDBC対応 ExampleのS2JDBCToDataAccessRule#insertでinsert関数が複数見つかる場合があったので試しにコード書いてみたらそうでした。Genericsはコンパイル時に型が解釈されるから当然なんだろうけど、普段コンパイルエラーで生成できないバイトコードが生成されるんですね。

以下、コードと実行結果。

  • S2AbstractService
// S2-TigerのS2AbstractServiceから抜粋
public abstract class S2AbstractService<T> {
    ... 省略 ...
    public int insert(T entity) {
        return jdbcManager.insert(entity).execute();
    }
    ... 省略 ...
}
  • AbstractService
public abstract class AbstractService<ENTITY> extends S2AbstractService<ENTITY> {
    public int insert(ENTITY entity) {
        return super.insert(entity);
    }
}
  • HogeService
public class HogeService extends AbstractService<Hoge> {
    @Override
    public int insert(Hoge hoge) {
        hoge.registTime = new Timestamp(new Date().getTime());
        return super.insert(hoge);
    }
    // insert(Hoge hoge)関数とObject型が重複するので自分で書くとコンパイルエラー
    // public int insert(Object object) {
    //     ...
    // }
}
  • 出力するためのコード、@Testついてるけど何もテストはしていない
@RunWith(Seasar2.class)
@RootDicon("app.dicon")
public class HogeServiceTest {
    @Test
    public void testFindInsertMethod() {
        Method methods[] = HogeService.class.getMethods();
        for (Method method : methods) {
            if (method.getName().startsWith("insert")) {
                System.out.println("# method: " + method.toGenericString());
            }
        }
        // Overrideされた関数があるかどうかを探します
        try {
            Method method =
                ClassUtil.getMethod(
                    HogeService.class,
                    "insert",
                    new Class[] { Hoge.class });
            if (method != null) {
                System.out.println("## found: " + method.toGenericString());
            }
        } catch (NoSuchMethodRuntimeException e) {
            System.out.println("## not found");
        }
    }
}
  • 出力結果
# method: public int org.example.service.HogeService.insert(org.example.entity.Hoge)
# method: public int org.example.service.HogeService.insert(java.lang.Object)
## found: public int org.example.service.HogeService.insert(org.example.entity.Hoge)

Buri + S2JDBC対応 Example

id:imai78さん某のブログのぶり入門記マトメ, id:j5ik2oさんのBuriをS2JDBC対応にしてみる その3, id:jfluteさんのDBFlute: Buri対応のプロトタイプ公開, [id:makotanさんのburi]新たに拡張ポイント追加 に触発されて作ってみました(ほとんどid:j5ik2oさんのS2JDBCToDataAccessRuleのおかげです)。

  • Buri + S2JDBC対応 Example の buri-example4 (Eclipseプロジェクトをexportしたもの)
    • [2009-03-13 11:17追記] HogeServiceにOverrideしたinsert関数があるとinsert用OGNL式が重複するのを修正 (S2JDBCToDataAccessRule#insertSetup)

Buriの内部でS2Daoが使用されているので依存ライブラリとしての2Daoは残っていますが、とりあえず利用者からはS2Dao意識しなくてOKです。DocumentProcessorTestで実行してみた感じ、たぶんちゃんと動いてそうです。詳細は家に帰った後にやる気があればかまた明日あたり書きました。

注意として、buri-core 1.0.1-SNAPSHOTはSVNから最新のコードをチェックアウトして自分のローカル環境に mvn install してください(最新のburi-core 1.0.1-SNAPSHOTをmvn deployして欲しいかも)。

[2009-03-12 00:16追記] ファイルの説明を追加

ファイルの説明。S2JDBC-Genで自動生成させるとBuri関係のEntity、Names、Serviceも生成されますが、判りやすいようにExampleでは削除してあります。

  • src/main/java
    • entity.Document
      • S2JDBC-Genで生成したEntityクラス、publicフィールドしかありません、デフォルトのままで改変なし
    • entity.names.DocumentNames
      • S2JDBC-Genで生成したNamesクラス、このExampleでは出番ないですが実際にアプリを書くときは大活躍します、デフォルトのままで改変なし
    • service.AbstractService
      • S2JDBC-Genで生成したものにS2JDBCToDataAccessRule用に public ENTITY select(Long id) を追加してあります
    • service.DocumentService
      • S2JDBC-Genで生成したServiceクラス、デフォルトのままで改変なし、実際にアプリを書くときはここに主にDBへの様々な方法でアクセスするためのコードを追加していきます
    • org.escafe.buri.compiler.util.impl.rules.DataAccessCheckRule
      • protected void checkKeyName(BuriDataFieldType src) でS2JDBC用に javax.persistence.Id アノテーションでプライマリキーを探すようにコードを追加してあります、buri-share.dicon をいじりたくなかったので同名クラスでごまかしています・・・要課題?
    • org.escafe.buri.compiler.util.impl.rules.S2JDBCToDataAccessRule
  • src/main/resources
    • buri/dicon/buri-user.dicon
      • id:makotanさんの新たに拡張ポイント追加で追加された userDataFieldRuleSet で S2JDBCToDataAccessRule を設定、ただ、diconファイルのinclude順の問題で、<include path="buri/dicon/event.dicon" /> と ClassDefUtilImpl のコンポーネント定義を追加
    • buri/dicon/selectByPath.sql
      • BuriPathDataを参照するS2JDBC用の2Way-SQLファイル、使い方は DocumentProcessorTest に
    • 他のdiconファイル
      • 普通のSMART deploy構成、ぶりと関係ないdiconファイルも入っていますが気にしないでください
  • src/test/resources
    • DocumentProcessorTest
      • 動作テスト用のテストクラス、id:imai78さんのぶり入門記ベースです

これで今やっている期限が来週の一人プロジェクトで楽が出来そうです。

なお、BuriAutoSelectProcessorでの簡単な動作確認しかしてないので、それ以外の複雑なことをしたらどのようになるかは試してみないと判らないです(^^;;。

寝込んで復活したらBuri周りが素敵なことになっていた

id:j5ik2oさんがBuriをS2JDBC対応に!素晴らしい!!

ParticipantのIDが反映されるように!素晴らしい!!

Buriを使ってみようか迷ってたところでしたが、使うしかないですね。

積もったタスク消化中: Hudson Plugin 完了

昨日作ったPublisherで、ジョブの設定ファイル(config.xml)に値を保存し、その値をcronで回して処理するスクリプトが完成しました。Seasarコミッタの方で、マルチジョブ実行時に複数軸用のテストデータベース環境が必要な方は、下記のページを参考に設定してご利用ください。

作ったHudson Plug-inは次のとおりです。本当はPlug-inから直接DB環境作った方が良いんでしょうが、Tomcatの動作権限の問題でこのPlug-inはほぼ何もしません。。なので見ても参考にならないでしょうが下記にアップしてあります。

<hudson.plugins.testdb.TestDBPublisher>
  <amountOfAdditionalTestDB>3</amountOfAdditionalTestDB>
</hudson.plugins.testdb.TestDBPublisher>

積もったタスク消化中: Hudson Plugin

Publisherで超簡単なの作ってみた。cronで回す連携用のスクリプトを明日作って完成かな。

さりげなく安全ではないMavenへの対策

将来的には安全になる様子。誰が署名するのってもあるけどとりあえずは。

ホーム > Java

検索
フィード
メタ情報

Return to page top