ログ日記

作業ログと日記とメモ

WicketでEntityからコンポーネントを自動生成+必須フィールド設定

GWTからWicketに移ろうとしていてちょっと感動している。
ラベルやフィールドを生成して無名クラスで拡張していくやり方はGWTと同じだから移行は問題なさそうだし、セッション周りの使い勝手が素晴らしい。


今気になるのはmountBookmarkablePageを羅列していくと多くなりすぎるのと、new Label や new TextField が多くなると大変なこと。
mountBookmarkablePageの羅列は、mount("/pages" PackageName.forClass(TopPage.class)) を使うと改善されるが、最初の "/pages" が不要なんだよね…。かと言って "/" にマウントしようとすると、そこはホームページですと言われて怒られる。どうしたもんか。


で、二つめのコンポーネントの生成が多いというのは、自動生成すればいいんじゃないかと。
こういうことは2年以上前に既に通った道らしくて
http://d.hatena.ne.jp/chimerast/20080807/1218090389
http://sourceforge.jp/projects/wicket-ja/lists/archive/user/2008-September/000226.html
この辺に書いてある。このときは賛否両論だったらしい。

質疑応答がかなり濃くて、

  • Wicketでせっかく定義ファイルからJavaへと制御をもどせたのにまた定義に戻すのか。Wicketの良さが消えないか
  • しかし実開発では常に一定数の初心者を抱えるので、コードを書ける側がそれを吸収してやらないといけない
  • しかしそれでは初心者は開発をおもしろいと思う余地がなくなるんではないか(つまりStrtusのころと同じになるじゃないか的な意見)

などバシバシ意見が飛び交っておもしろかったです。

http://d.hatena.ne.jp/t_yano/20080804/1217823773

ということらしい。確かに。
そもそも必須項目や文字列の長さの制限は誰が決めるものなんだろう?フォームをデザインする人?Javaでフォームに関連するPageクラスを書く人?
この辺はまだよく分からないので保留にしているが、一つだけ確かなことはデータベースがnot nullならそれは確実に必須項目だということ。


というわけで作りかけのコンポーネント自動生成のクラス。

/**
 * エンティティクラスから自動でコンポーネントを生成してaddするコンテナ。
 *
 * <pre>有効な属性
 * - attr:multiline="true"  MultiLineLabelを生成する。
 * </pre>
 * @author nishimura
 * @param <T> Entityクラス
 */
@SuppressWarnings("serial")
public class EntityComponent<T> extends WebMarkupContainer implements IComponentResolver {
    private static final String MULTI_LINE = "attr:multiline";

    private T entity;
    public EntityComponent(String id, T obj) {
        super(id, new CompoundPropertyModel<T>(obj));
        this.entity = obj;
    }

    @Override
    public boolean resolve(MarkupContainer container, MarkupStream stream,
            ComponentTag tag) {
        if (tag.isAutoComponentTag())
            return false; // wicketが自動で追加するタグ

        String tagName = tag.getName().toLowerCase();
        if (tagName.equals("input")){
            return addFormComponent(new TextField<Void>(tag.getId()), tag, container, stream);

        }else if (tagName.equals("textarea")){
            return addFormComponent(new TextArea<Void>(tag.getId()), tag, container, stream);

        }else{
            String multiline = tag.getAttribute(MULTI_LINE);
            if (multiline != null && multiline.equals("true"))
                return container.autoAdd(new MultiLineLabel(tag.getId()), stream);
            else
                return container.autoAdd(new Label(tag.getId()), stream);
        }

    }
    /**
     * Entityのアノテーションを見て制約を設定する。
     * @param component
     * @param tag
     * @return
     */
    private boolean addFormComponent(FormComponent<?> component, ComponentTag tag,
            MarkupContainer container, MarkupStream markupStream){
        try {
            Field f = entity.getClass().getField(tag.getId());
            Column col = f.getAnnotation(Column.class);
            if (col != null){
                if (!col.nullable())
                    component.setRequired(true);
            }
        } catch (SecurityException e) {
        } catch (NoSuchFieldException e) {
        }
        return addForRender(component, container, markupStream);
    }

    /**
     * 入力値でモデルを更新するためにはautoAddは使えないので、form用autoAddメソッド。
     * @param component
     * @param container
     * @param markupStream
     * @return
     */
    private boolean addForRender(Component component, MarkupContainer container,
            MarkupStream markupStream){
        container.internalAdd(component);
        component.prepareForRender();
        try {
            if (markupStream == null){
                component.render();
            }else{
                component.render(markupStream);
            }
        } finally {
            component.afterRender();
        }
        return true;
    }
}

まだフォームのコンポーネントはinput type="text"以外には対応してないけれど、考えメモ的な。


これを利用するページクラスのコードはこんな感じになる。

public class ItemFormPage extends WebPage {
    @Resource
    private ItemService itemService;

    @SuppressWarnings("serial")
    public ItemFormPage(final PageParameters params){
        // idがなければ表示ページへ
        super(params);
        if (params == null)
            throw new RestartResponseException(ItemPage.class);
        final Integer itemId = params.getAsInteger("itemId");
        if (itemId == null)
            throw new RestartResponseException(ItemPage.class);

        final Item item = itemService.findById(itemId, MySession.get().getAccountId());

        Form<Void> form = new Form<Void>("form"){
            @Override
            protected void onSubmit(){
                MySession session = MySession.get();
                if (!item.accountId.equals(session.getAccountId()))
                    throw new RestartResponseException(SessionErrorPage.class);

                try {
                    itemService.update(item);
                    session.info(getString("success"));
                    setResponsePage(ItemPage.class, params);
                }catch (Exception e){
                    warn(getString("error"));
                }
            }
        };
        EntityComponent<Item> component = new EntityComponent<Item>("item", item);
        form.add(component);
        add(form);
    }
}


サービスやエンティティはs2jdbc-genを使って生成したものをそのまま使う。

public class Item implements Serializable {

    private static final long serialVersionUID = 1L;

    /** itemIdプロパティ */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(precision = 10, nullable = false, unique = true)
    public Integer itemId;

    /** accountIdプロパティ */
    @Column(precision = 10, nullable = false, unique = false)
    public Integer accountId;

    /** nameプロパティ */
    @Lob
    @Column(length = 2147483647, nullable = false, unique = false)
    public String name;
...

ここに nullable = false と書いてあるので、先ほどの自動生成箇所では、このColumnアノテーションを見てsetRequiredを設定している。
これならJavaからテキストファイルに戻ったりはしていないし、nullable = falseならどんなフォームだろうと管理者特権だろうと必ず必須項目のはずだ。


あとは、メールアドレスのチェックとか文字数制限もアノテーションでEntityに指定すればいいんじゃないかな?
例外としてデータベースに直結しない項目はEntityComponent#add でバリデーションを設定したコンポーネントを直接追加していく方向で。