Teedaでネストしたプロパティー表示の実験

概要

Teedaを利用していて、面倒だなって思うところでDtoなどの大量のデータを表示する際にページプロパティーに詰め替える作業があります。Teedaの次バージョンでは実装予定項目で入っていますが、まだまだ出そうにないので実験してみました。

目的

実用的に利用できる状態までの実験は行わず、実装方法の検証とどれぐらいの作業量がかかりそうかの検証を目的とします。

書式の規定

通常Teedaはテンプレートのidにプロパティー名を入れます。ネストした場合には「neko.itemName」とするのが妥当なのですが、"."(ピリオド)を利用するとJavaScriptなどでスタイルシートの"."(ピリオド)との区別が付かなくなります。JSFデフォルトの":"(コロン)と同じくあまり好ましくない書式になります。

idに使わない記号で、あまり違和感のない"_"(アンダーバー)を今回は利用したいと思います。Teedaの場合通常ローワー・キャメルケースで書くので"_"(アンダーバー)は使いませんよね?

ただし、JSFの実装理念的には"."(ピリオド)を使うのが正しいはずです!

<span id="nekoDto_nekoString">nekoStringDummy</span><br />
<span id="nekoDto_nekoInt">nekoIntDummy</span><br />

こんな感じで"_"(アンダーバー)がきたら、そのクラスのプロパティーを表示するようにしたいと思います。

準備

ソースのダウンロード

Teeda本体をコンパイルするのにSeasar2本体も必要なので同時にダインロードしましょう。

JDKの設定

Seasarプロダクトは基本的にJDK4かJDK5でコンパイルを行います。Tigerと付いているパッケージは5でそれ以外が4の事が多いようです。今回はJDK5でコンパイルを行いたいと思います。

また、JDK6だとコンパイルエラーがでるのでコンパイルを行うことができません!

JAVA_HOME=C:\Program Files\Java\jdk1.5.0_16

JDK5の指定を行います。またコンパイル等はコマンドプロンプト上のmaven2を利用して行いました。

パッケージのテスト
mvn package

を実行してパッケージが成功するか確かめましょう。

該当場所のソース検索

表示にかかわる場所はすべて「teeda-extension」以下にありますが、そのものずばりのソースの場所がわからないので、まずは「label」をキーワードにしてソースを眺めてみました。

LabelProviderMap.java

    public Object get(final Object pageName) {
        return new LabelProviderMap() {
            public Object get(final Object key) {
                final String label = LabelUtil.getLabelValue((String) key,
                        (String) pageName);
                if (!StringUtil.isEmpty(label) || suppressDecolate) {
                    return label;
                }
                return "??" + key + ExtensionConstants.LABEL_ATTRIBUTE_SUFFIX +
                        "??";
            }
        };
    }

ラベルの指定をミスると??で囲われて出力されるので、実際にはここでリストから取ってきているようです。ただ今回はあまり関係なさそうですね。

AbstractElementProcessorFactory.java

    protected String getLabelExpression(final String attributeValue,
            PageDesc pageDesc) {
        final String pageName = pageDesc.getPageName();
        final String labelName = attributeValue.substring(0, attributeValue
                .length() -
                ExtensionConstants.LABEL_ATTRIBUTE_SUFFIX.length());
        return "#{labelProvider." + pageName + "." + labelName + "}";
    }

ここで上のプロバイダーを利用してラベルの出力を行っているようです。#{}の形式ですのでJSFで出力しているんですね。

OutputTextFactory.java

    public boolean isMatch(ElementNode elementNode, PageDesc pageDesc,
            ActionDesc actionDesc) {
        final String tagName = elementNode.getTagName();
        if (TeedaExtensionConfiguration.getInstance().outputTextSpanOnly &&
                !tagName.equals(JsfConstants.SPAN_ELEM)) {
            return false;
        } else if (!acceptableElements.contains(tagName.toLowerCase())) {
            return false;
        }
        if (pageDesc == null) {
            return false;
        }
        final String id = elementNode.getId();
        if (id == null) {
            return false;
        }
        if (isLabel(id, elementNode)) {
            return true;
        }
        return pageDesc.hasProperty(id);
    }

isLabelという名前を発見。なんかここが怪しいですね!

    protected boolean isLabel(final String id, final ElementNode elementNode) {
        final String key = toNormalizeId(id);
        if (!TeedaExtensionConfiguration.getInstance().outputTextLabelUnderAnchorOnly) {
            return key.endsWith("Label");
        }

        final ElementNode parent = elementNode.getParent();
        if (parent == null) {
            return false;
        }
        final String tagName = parent.getTagName();
        if (key.endsWith("Label") &&
                tagName.equalsIgnoreCase(JsfConstants.ANCHOR_ELEM)) {
            return true;
        } else {
            return false;
        }
    }

えーっと、処理内容的には最後にLabelが付いた場合にtrueになっていますね。isMatchという名前を見る限りテキスト出力時にidが処理対象かを判断する場所と思われます!

ここを拡張してネストしたプロパティーを実装したいと思います。

実装の追加

ネストチェックロジック追加
    protected boolean isNest(final String id, PageDesc pageDesc) {
        String[] items = id.split("_");

        if( items.length != 2 ){
            return false;
        }

        return pageDesc.hasProperty(items[0]);
    }

isNestという名前でチェック関数の追加をします。内容的には"_"(アンダーバー)が1つ含まれている場合で、そのクラスがページプロパティーにある場合にtrueとなります。本当はネストした先の存在チェックとかも必要ですし、複数ネストした状況とかもあるのですが実験はここまで。。。

idチェックロジック修正
    public boolean isMatch(ElementNode elementNode, PageDesc pageDesc,
            ActionDesc actionDesc) {
        final String tagName = elementNode.getTagName();
        if (TeedaExtensionConfiguration.getInstance().outputTextSpanOnly &&
                !tagName.equals(JsfConstants.SPAN_ELEM)) {
            return false;
        } else if (!acceptableElements.contains(tagName.toLowerCase())) {
            return false;
        }
        if (pageDesc == null) {
            return false;
        }
        final String id = elementNode.getId();
        if (id == null) {
            return false;
        }
        if (isLabel(id, elementNode)) {
            return true;
        }
        if (isNest(id, pageDesc)) {
            return true;
        }
        return pageDesc.hasProperty(id);
    }

isNestをチェック関数に追加します。これでネストしたid形式の場合OutputTextで利用されるようになりました。

テストプロジェクトの準備

ここで一度テスト用のプロジェクトを作成して、実験してみたいと思います。

プロジェクト作成

Doltengを利用してTeedaプロジェクトを作成します。

Dto作成

NekoDto.java

package neko.web;

public class NekoDto {
    public int nekoInt;
    public String nekoString;
}

シンプルなDtoクラスを作成します。privateでもかまいませんがその場合にはSetterとGetterを作成してあげてください。

ページクラス作成

TestPage.java

package neko.web;

public class TestPage {

    public NekoDto nekoDto;

    public Class<?> initialize() {
        nekoDto = new NekoDto();
        nekoDto.nekoInt = 12345;
        nekoDto.nekoString = "neko1234";

        return null;
    }

    public Class<?> prerender() {
        return null;
    }
}

こちらもシンプルにDtoの作成と値設定のみを行います。

テンプレート作成

test.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:te="http://www.seasar.org/teeda/extension" lang="ja" xml:lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>nekoTest</title>
</head>
<body>

Teeda[nekoString]:<span id="nekoDto_nekoString">nekoStringDummy</span><br />
Teeda[nekoInt]:<span id="nekoDto_nekoInt">nekoIntDummy</span><br />

</body></html>

ネストの指定が入っている以外は通常のXHTMLで問題ありません。

自作Teedaパッケージへ入れ替え

新規作成の状態では最新のTeedaのパッケージが利用されるので、自分でパッケージしたファイルを利用するように変更します。

copy /y teeda-ajax\target\teeda-ajax-1.1.0-alpha1-SNAPSHOT.jar ..\neko\src\main\webapp\WEB-INF\lib
copy /y teeda-core\target\teeda-core-1.1.0-alpha1-SNAPSHOT.jar ..\neko\src\main\webapp\WEB-INF\lib
copy /y teeda-extension\target\teeda-extension-1.1.0-alpha1-SNAPSHOT.jar ..\neko\src\main\webapp\WEB-INF\lib

なんのひねりもないですが、自分でパッケージしたファイルをテスト用のプロジェクトにコピーして、元々あるパッケージを削除してコピーしたパッケージを利用するようにクラスパスを書き換えます。

実行

Teeda[nekoString]:??nekoDto_nekoSLabel??
Teeda[nekoInt]:??nekoDto_neLabel??

当たり前ですが、出力がおかしいです。OutputTextで出力部分の実装を行っていないですからね。ただlabelの処理系に飛んでいるってことで、認識までは動く動作していることが確認できました。

出力部分実装

ここはかなりいろいろ試行錯誤して、どこにいれればいいのか悩んだのですが、、、

OutputTextFactory.java

    protected void customizeProperties(Map properties, ElementNode elementNode,
            PageDesc pageDesc, ActionDesc actionDesc) {
        super
                .customizeProperties(properties, elementNode, pageDesc,
                        actionDesc);
        properties.put("tagName", elementNode.getTagName());

        if (pageDesc == null) {
            return;
        }
        final String id = elementNode.getId();
        if (pageDesc.hasProperty(id)) {
            properties.put(JsfConstants.VALUE_ATTR, getBindingExpression(
                    pageDesc.getPageName(), id));
        } else {
            final String key = toNormalizeId(id);
            TextNode firstTextNode = elementNode.getFirstTextNode();
            properties.put(JsfConstants.VALUE_ATTR, getLabelExpression(key,
                    pageDesc));
        }
    }

灯台下暗し・・・isLabelのすぐ上になった関数で処理していました!

処理的には「pageDesc.hasProperty(id)」でページプロパティーの場合にはidを出力して、それ以外はlabelを出力するってなっていますね。

    protected void customizeProperties(Map properties, ElementNode elementNode,
            PageDesc pageDesc, ActionDesc actionDesc) {
        super
                .customizeProperties(properties, elementNode, pageDesc,
                        actionDesc);
        properties.put("tagName", elementNode.getTagName());

        if (pageDesc == null) {
            return;
        }
        final String id = elementNode.getId();
        if (pageDesc.hasProperty(id)) {
            properties.put(JsfConstants.VALUE_ATTR, getBindingExpression(
                    pageDesc.getPageName(), id));
        } else if( isNest(id, pageDesc) ){
            String[] items = id.split("_");
            properties.put(JsfConstants.VALUE_ATTR, getBindingExpression(
                    pageDesc.getPageName(), items[0]+"."+items[1]));
        } else {
            final String key = toNormalizeId(id);
            TextNode firstTextNode = elementNode.getFirstTextNode();
            properties.put(JsfConstants.VALUE_ATTR, getLabelExpression(key,
                    pageDesc));
        }
    }

改造して、isNestの場合に「nekoDto_nekoSLabel」を「nekoDto.nekoSLabel」に置換して出力しています。

実行

  • パッケージ作成(mvn package)
  • jarファイルコピー(copy -y...)
  • Tomcat再起動

の手順を踏んでからページを再表示させます。

Teeda[nekoString]:neko1234
Teeda[nekoInt]:12345

見事表示されました!

仕組み

元々JSFではネストしたプロパティーを実は表示することができます。そのため案外改造箇所が少なく動いています。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:te="http://www.seasar.org/teeda/extension" xmlns:h="http://java.sun.com/jsf/html" lang="ja" xml:lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>nekoTest</title>
</head>
<body>

Teeda[nekoString]:<span id="nekoDto_nekoString">nekoStringDummy</span><br />
Teeda[nekoInt]:<span id="nekoDto_nekoInt">nekoIntDummy</span><br />

<br />

JSF[nekoString]:<h:outputText value="#{testPage.nekoDto.nekoString}"/><br />
JSF[nekoInt]:<h:outputText value="#{testPage.nekoDto.nekoInt}"/><br />

</body></html>

こんな感じで最初に「xmlns:h="http://java.sun.com/jsf/html"」を追加してJSFのタグを利用可能にして、h:outputTextを直接呼べば無改造のTeedaでもネストしたプロパティーへアクセスすることができます。

現状はネストしたidの場合ページプロパティーに存在していないと思い、labelの処理に飛んでいるので表示することができませんでした。

総括

簡単にできているように書いてありますが、予想以上に解析に時間かかっていたりします(笑) 結局6時間前後かかったよ。。。
outputTextは簡単にできているように見えますが、実運用するためにはもう少しきれいに組む必要があります。。。InputTextとか大量に対応しないといけない場所があるので、ちょっと面倒かも。ただ技術的難易度はそれほどない気がしますので、手間をかければ実装できそうです。

ただTeedaプロジェクトの人は現在T2に注力している感じなので、当分機能追加のバージョンアップはなさそうですね。個人的にはこのネストがあるとページクラスが非常にすっきりするので欲しい機能だったりします。

うーむ、自分で組むかが悩ましい。。。