key(グルーピング)

XML文書を、特定の値をもつ集合で、グルーピングする処理です。
xslt で実現するのは、それほど難しくは無いのですが、JSTL:X タグの方は、落とし穴だらけでした。
今週は、何とか動くコードをつくるところで、力尽きてしまい、ソースの解析は、来週にさせていただきます。

結論

XSLTを使いましょう。
JSTL:X タグでは、落とし穴だらけで、あまりお勧めできません。

ソースのダウンロード

JSP-XML-2007-11-19.zipをダウンロードしてください。
いつものように、Maven2 でビルドして、Tomcatにデプロイして、http://localhost:8080/JSP-XML/を開きます。
トップページのドロップダウンリストに追加されている「key(グルーピング)」を選んで、「JSTL Xタグで変換」または
「XSLT で変換」のボタンを押してください。

結果は、JSTL Xタグでも、XSLTでも、下図のように表示されます。

変換内容

下記の XML文書で、果物の 生産国( item/from ), 果物名( item/name ) を列挙した item 群を
item/from でグルーピングして、HTMLの表にします。

変換する上での留意点は、下記の項目です。

  • fromにどんな値が入っているか、事前には分からない
  • HTMLでは、td の rowspan 属性で、整形する
  • 今週はここまで

    今週は、動くソースコードを作ったところで力尽きてしまいました。
    ソースの解析は、来週やります。

    JSTL:X タグのポイント

    それでは、落とし穴の多かった JSTL:X タグの解説を行います。
    ソースは、/jsp/jstl/key.jspです。

    最初に、元のXML文書のfromを、重複なく取り出すことにします。
    そのため、冒頭でHashSet を宣言して、XMLのfromの値を格納していきます。

    次に、テーブルの <tr>行を作る箇所です。
    <c:forEach> で、冒頭に作った HashSet から値を1つずつ取り出して
    ループをまわします。

    落とし穴1

    1つの国に 2つ以上のフルーツがあれば、HTML的には <col rowspan="個数">で、
    見栄えを整えるため、XML文書中に 同じ国の 要素がいくつあるかカウントします。
    <x:set var="num" select="count($domXML/item[ from = $key ])"/>
    この表現って、なんかちょっと違和感ありませんか?
    例えば、国が日本である item は、select="$domXML/fruits/item[ from = '日本' ]" で表します。
    違和感とは、JSTL:Xタグの count 関数には fruits の層を指定しないのです。
    このことに気が付くまで、かなりの時間を浪費しました。

    落とし穴2

    次に、国別ループの中で、さらにフルーツ毎のループを行います。
    1回目のループでは、
    <tr><td rowspan="個数">国名</td><td>フルーツ名</td></tr>
    2回目以降のループでは、
    <tr><td>フルーツ名</td></tr>
    とするために、そのフルーツが 1回目か 2回目かを判定する必要があります。
    そこで、最初は <x:forEach var="data" select="$domXML/fruits/item[ from = $key ]">の内部で
    <x:if test="oosition() = 1"> という if文を使うことにしたのですが、なんとJSTL:Xタグでは、
    position()の値はいつも -1 という驚愕の落とし穴にはまりました。
    結局は、>c:set var="loop" value="1"/< で、自分でループ変数を宣言して、自分でインクリメントして
    position()関数の代用にしました。

    落とし穴だらけでしたが、JSTL:X タグによる <table>作成部を下に掲載します。

    XSLTのポイント

    ポイントになるソースは /jsp/xslt/xsl/key.xsl です。
    XSLTでは、グルーピングということで、xsl:key を使ってみました。
    冒頭で、以下のように宣言します。 <xsl:key name="key-from" match="/fruits/item" use="from"/>
    この後は、select="key( 'key-from', '国名')" とすると、指定した '国名' を持つ
    item ノードの集合を取得することができます。

    こちらも、国別にループをまわして、その中で同じ国をもつitemでさらにループします。
    国別ループの中で、<xsl:if test="not ( boolean( preceding-sibling::item[ from = $from]) )">
    というif文がありますね。
    これは、preceding-sibling::item という XPath のキーワードで、対象としているノード( item )よりも
    XML文書で先に出現した 同階層の item 全てという表現になります。
    これにより、同じ国のフルーツは、その国が2回目以降は if文の中に入らなくなります。

    あとは、rowspan を意識して、各<tr> を記述していきます。
    <table>を作成する箇所を、下記に記します。

    ところで、気がついた人もいるかもしれませんが、今回のXSLTでは、xsl:keyは
    実は使わなくても実現できてしまいます。具体的には、以下のように書き換えます。

    (書換え前) select="count(key( 'key-from', $from ))"
    (書換え後) select="count( /fruits/item[ from = $from ] )"

    (書換え前) select="key( 'key-from', $from )"
    (書換え後) select="/fruits/item[ from = $from ]"

    まとめ

    いかがでしたでしょうか。今回は JSTL:X タグに悩まれた課題でした。
    まあ、落とし穴さえ分かってしまえば、どちらで実装しても難易度に差はないようには
    感じます。