argius note

プログラミング関連

Wicketで真のステートレスページ

Wicketはステートフルが基本。
それでも、ステートレスなページをWicketで実現したい、実現できるなら多少制限があってもかまわない、という場合の、真のステートレス(と言い切っていいものかどうか)を実現する方法を模索してみました。


何度も書いているように、WicketXMLレス+継承できるHTMLテンプレートなど、とても使いやすいので、ステートフルのためだけに捨てるのは非常に惜しい。
インスタンスメソッドである必要が無いメソッドはクラスメソッドで実装するように、状態が不要なケースもあります。たとえば、検索結果の2ページ目をブックマークして、そのブックマーク(URL)で直接その検索結果の2ページ目に飛べるようにする、のようなケース。これをステートフルでやってみると少し不都合があります。


素のWicket(1.5.3)で「ステートレス」にする方法は

  • Page#isPageStatelessがtrueになるようにする
    • Page#getStatelessHint()がtrueになるようにする
    • Pageが持つBehavior(Form,Linkなど)がすべてstatelessである

となります。
具体的には、FormはStatelessFormを、LinkはBookmarkablePageLinkを使い、PageではコンストラクタでsetStatelessHint(true)を実行すればPage#isPageStatelessがtrue、すなわち「ステートレス」になります。


ここで問題となるのが、StatelessFormのアクションがステートレスではないということです。(LinkにおけるStatelessLinkも同様。)
詳しくはStatelessFormが生成するHTMLコードを見てもらえば分かりますが、検索後のURLが

/search?3&query=java
(form method=get)

のようにステートレスではない状態になります。submit時の振る舞いもステートフル(dispatchするなど)になります。ただ、このURLに直接飛んだ場合の動きはステートレスです。これでも良ければこの話は終わりです。
これを、たとえば多くの検索サイトのように、検索結果ページのURLをそのままブックマークするときに、"3&"のところは出来れば無しにしたい場合。


いろいろと試行錯誤した結果、新たなStatelessFormを作ってしまうことで解決しました。
やっていることは、タグのレンダリングで生成しているactionのpathやhiddenタグの生成を抑制しているだけです。これでformタグはほぼテンプレートに書いたままの状態、つまり、submit時の振る舞いがURLを直接入力したのと同じようになります。

public class BookmarkableForm<T> extends Form<T> {

    private String actionPath;

    public BookmarkableForm(String id) {
        this(id, "");
    }

    public BookmarkableForm(String id, String actionPath) {
        super(id);
        this.actionPath = actionPath;
    }

    @Override
    protected boolean getStatelessHint() {
        return true;
    }

    @Override
    protected void onComponentTag(ComponentTag tag) {
        tag.put("action", actionPath);
    }

    @Override
    public void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {
        if (isRootForm()) {
            StringBuilder buffer = new StringBuilder();
            getResponse().write(buffer);
        }
        renderComponentTagBody(markupStream, openTag);
    }

    private final void renderComponentTagBody(final MarkupStream markupStream,
                                              final ComponentTag openTag) {
        if ((markupStream != null) && (markupStream.getCurrentIndex() > 0)) {
            ComponentTag origOpenTag = (ComponentTag)markupStream.get(markupStream.getCurrentIndex() - 1);
            if (origOpenTag.isOpenClose()) {
                return;
            }
        }
        boolean render = openTag.requiresCloseTag();
        if (render == false) {
            render = !openTag.hasNoCloseTag();
        }
        if (render == true) {
            renderAll(markupStream, openTag);
        }
    }

}

このFormでsubmitした結果は、

/search?query=java

となります。dispatchも発生しません。


PagingNavigatorも組み合わせたい場合は、BookmarkableNavigatorを見てみてください。