コード値の表示をtaglibでおこなう

Strutsでデータベースの内容を表示するとき、コード(ID値)で格納されている値を名称で表示したいことって結構あります。
(例えば性別を1→「男」、2→「女」で表示するとか)


アクションでデータを取得した際に処理を書いて名称に変換したり、名称に変換するプログラムを書いてHTMLテンプレートから呼び出したりという処理をいちいち書くのは面倒くさいので、そういうtaglib(LabelCollectionTag.java)を書いてJSP上だけで処理出来るようにしてみた。(この辺で書いていた事です。)


処理の内容としては、StrutsのtaglibのoptionsCollectionで選択されている値の場合に"selected"が付くのと同じような感じ。ソースコードもほぼOptionsCollectionTag.java付近からパクってきた。

使い方

社員一覧ページで血液型を表示する場合を考える。社員情報にはIDだけが格納されていて、IDと名称の対応はDBじゃなくアプリ内部に保持しているとする。ここでは、EnumとしてBloodTypeが定義されている場合を想定する

package sample.model;

public enum BloodType {
    A (1, "A型"),
    B (2, "B型"),
    O (3, "O型"),
    AB (4, "AB型");
    
    /** ID値 */
    private int value;
    /** ラベル名 */
    private String label;

    BloodType(int value, String label) {
        this.value = value;
        this.label = label;
    }
    public int getValue() {
        return value;
    }
    public String getLabel() {
        return label;
    }
}


社員の一覧ページで、requestにキー"employees"で格納されている社員情報のListを表示するには、テンプレート(JSPなど)でtaglibを使って以下のように記述

<%@page import="sample.model.BloodType"%>
<%@ taglib uri="http://terazzo.dyndns.org/tags-sample" prefix="sample" %>
...
<% request.setAttribute("bloodTypes", BloodType.values()); %>
<table>
  <tr>
    <th>社員番号</th><th>名前</th><th>血液型</th>
  </tr>
  <logic:iterate name="employees" id="employee">
  <tr>
    <td><bean:write name="employee" property="id"/></td>
    <td><bean:write name="employee" property="name"/></td>
    <td>
      <sample:labelCollection
          collectionName="bloodTypes" value="value" label="label"
          name="employee" property="bloodType"/>
    </td>
  </tr>
  </logic:iterate>
</table>

collectionName, collectionPropertyで指定されたコレクションの要素のvalueで指定されたプロパティ値と、name, propertyで指定された値が一致した場合、その要素のlabelで指定したプロパティ値が表示される

出力結果例:

社員番号 名前 血液型
1 青木 O型
2 長嶋 A型
3 山本 B型
4 二ノ宮 AB型
5 鈴木 O型
6 M田中 A型

taglibのソースコード

package sample.struts.taglib;

import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.TagSupport;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.struts.taglib.TagUtils;
import org.apache.struts.util.IteratorAdapter;
import org.apache.struts.util.MessageResources;

/**
 * <p>リストと選択値からラベル文字列を表示する部品</p>
 * 使い方:<ol>
 * <li>collectionName, collectionPropertyでコレクションを指定する。この
 * 場合のコレクションはoptionsCollectionsTagと同様に配列やMapも指定できる</li>
 * <li>name, propertyで選択されている値を指定する</li>
 * <li>コレクションの各要素から、値を取得するキーをvalueで指定する</li>
 * <li>コレクションの各要素から、表示する文字列を取得するキーをlabelで指定する</li>
 * <li>コレクションの各要素のvalueで指定されたプロパティの値と、name,property
 * で指定された選択値が等しいと判定された場合、その要素のlabelプロパティの値が出力される</li>
 * <li>出力文字列をタグで得たい場合(divやspanなど)、tagName, style, styleClassを指定する。</li>
 * </ol>
 */
public class LabelCollectionTag extends TagSupport {
    /** シリアルバージョン */
    private static final long serialVersionUID = 1825831286902847511L;
    /** MessageResources */
    private static MessageResources messages = MessageResources.
            getMessageResources("sample.struts.taglib.LocalStrings");
    /** HTMLとしてエスケープするかどうかのフラグ */
    private boolean filter;
    /** リストを取得する為のBean名 */
    private String collectionName;
    /** リストを取得する為のプロパティ名 */
    private String collectionProperty;
    /** 選択値を取得する為のBean名 */
    private String name;
    /** 選択値を取得する為のプロパティ名 */
    private String property;
    /** リストの要素から表示する文字列を取得するキー(プロパティ名) */
    private String label;
    /** リストの要素から値を取得するキー(プロパティ名) */
    private String value;
    /** div,spanなどの出力タグ名。省略時はタグで囲まない */
    private String tagName;
    /** 出力タグに指定するstyle属性の値。tagName省略時は出力しない*/
    private String style;
    /** 出力タグに指定するclass属性の値。tagName省略時は出力しない*/
    private String styleClass;

    /**
     * doStartTagを実装する
     * @return 常にSKIP_BODYを戻す
     * @throws JspException タグ処理中の例外
     */
    public int doStartTag() throws JspException {
        // collectionの値を取得
        Object collection = TagUtils.getInstance().lookup(super.pageContext,
                collectionName, collectionProperty, null);
        if (collection == null) {
            JspException e = new JspException(
                    messages.getMessage("labelCollectionTag.collection"));
            TagUtils.getInstance().saveException(super.pageContext, e);
            throw e;
        }
        // 選択されている値を取得
        Object selectedValue = TagUtils.getInstance().lookup(super.pageContext,
                name, property, null);
        StringBuffer sb = new StringBuffer();
        
        // Collectionから選択されている値に一致する要素を選び、表示
        Iterator iter = getIterator(collection);
        while (iter.hasNext()) {
            Object bean = iter.next();
            Object beanValue = getBeanValue(bean, value);
            if (isMatched(beanValue, selectedValue)) {
                Object beanLabel = getBeanValue(bean, label);
                writeLabel(sb, beanLabel.toString());
                break;
            }
        }

        TagUtils.getInstance().write(super.pageContext, sb.toString());
        return SKIP_BODY;
    }

    /**
     * ラベル文字列を表示する
     * @param sb 文字列を出力するStringBuffer
     * @param beanLabel 表示するラベル文字列
     */
    private void writeLabel(final StringBuffer sb, final String beanLabel) {
        if (this.tagName != null) {
            sb.append("<").append(tagName).append(" ");
            if (style != null) {
                sb.append(" style=\"");
                sb.append(style);
                sb.append("\"");
            }
            if (styleClass != null) {
                sb.append(" class=\"");
                sb.append(styleClass);
                sb.append("\"");
            }
            sb.append(">");
        }
        if (filter) {
            sb.append(TagUtils.getInstance().filter(beanLabel));
        } else {
            sb.append(beanLabel);
        }
        if (this.tagName != null) {
            sb.append("</").append(tagName).append(">\r\n");
        }
    }


    /**
     * 選択された値かどうかを判断する
     * @param beanValue リスト中の値
     * @param selectedValue 選択中の値
     * @return 両方がnullまたは一致した場合trueを戻す
     */
    private boolean isMatched(final Object beanValue, final Object selectedValue) {
        if (beanValue == null && beanValue == null) {
            return true;
        }
        if (beanValue == null || beanValue == null)  {
            return false;
        }
        return beanValue.toString().equals(selectedValue.toString());
    }


    /**
     * オブジェクトbeanからプロパティ名nameに対するプロパティ値を取得する
     * @param bean オブジェクト
     * @param name プロパティ名
     * @return 取得したプロパティ値
     * @throws JspException プロパティ値取得失敗時の例外
     */
    private Object getBeanValue(final Object bean, final String name)
            throws JspException {
        Object beanValue;
        try {
            beanValue = PropertyUtils.getProperty(bean, name);
            if (beanValue == null) {
                beanValue = "";
            }
        } catch (IllegalAccessException e) {
            JspException jspe = new JspException(messages.getMessage(
                    "getter.access", name, bean));
            TagUtils.getInstance().saveException(super.pageContext, jspe);
            throw jspe;
        } catch (InvocationTargetException e) {
            Throwable t = e.getTargetException();
            JspException jspe = new JspException(messages.getMessage(
                    "getter.result", name, t.toString()));
            TagUtils.getInstance().saveException(super.pageContext, jspe);
            throw jspe;
        } catch (NoSuchMethodException e) {
            JspException jspe = new JspException(messages.getMessage(
                    "getter.method", name, bean));
            TagUtils.getInstance().saveException(super.pageContext, jspe);
            throw jspe;
        }
        return beanValue;
    }

    /**
     * 引数collectionの自然なIteratorを取得する
     * @param collection Array,Collection, Iterator, Map, Enumerationなどのインスタンス
     * @return collection で表わされるCollectionのIterator
     * @throws JspException collectionがArray,Collection, Iterator,
     *  Map, Enumerationなどでない場合の例外
     */
    protected Iterator getIterator(final Object collection) throws JspException {
        Object theCollection = collection;
        if (theCollection.getClass().isArray()) {
            theCollection = Arrays.asList((Object[]) theCollection);
        }
        if (theCollection instanceof Collection) {
            return ((Collection) theCollection).iterator();
        }
        if (theCollection instanceof Iterator) {
            return (Iterator) theCollection;
        }
        if (theCollection instanceof Map) {
            return ((Map) theCollection).entrySet().iterator();
        }
        if (theCollection instanceof Enumeration) {
            return new IteratorAdapter((Enumeration) theCollection);
        } else {
            throw new JspException(messages.getMessage(
                    "labelCollectionTag.iterator", theCollection.toString()));
        }
    }
    
    /**
     * release
     */
    public void release() {
        super.release();
        filter = true;
        label = "label";
        name = "org.apache.struts.taglib.html.BEAN";
        property = null;
        collectionName = null;
        collectionProperty = null;
        tagName = null;
        style = null;
        styleClass = null;
        value = "value";
    }

    /* 以下アクセサメソッド */
...
}

WEB-INF/tld/sample.tld:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE taglib
        PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"
        "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd">
<taglib>
    <tlib-version>1.3</tlib-version>
    <jsp-version>1.2</jsp-version>
    <short-name>sample</short-name>
    <uri>http://terazzo.dyndns.org/tags-sample</uri>
    <tag>
        <name>labelCollection</name>
        <tag-class>
            sample.struts.taglib.LabelCollectionTag</tag-class>
        <body-content>empty</body-content>
        <attribute>
            <name>value</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <name>label</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <name>name</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <name>property</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <name>collectionName</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <name>collectionProperty</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <name>tagName</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <name>filter</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
            <type>boolean</type>
        </attribute>
        <attribute>
            <name>style</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <name>styleClass</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
    </tag>
</taglib>

動作環境

課題

  • マッチングはループによる逐次検索が使われているので、何万件もある場合は別の実装を考える必要がある。

その他

最近も相変わらずStrutsの仕事をしているんだけど、我ながらアクションやアクションフォームに沢山処理を書き過ぎだと思う。


いわゆるプレゼンテーションロジックって何だろうと思うんだけど、IDを名称に変換したり、一覧データを適当にグループ分けして表示したり、そんなのってロジックって呼べるんだろうか。少なくとも、毎回プログラムを書き下ろす価値がある処理なんだろうか。


IDを名称に変換する処理で言えば、両方ともデータベースに入っているのなら最初からリレーションで取得するという方法もある。でもその場合、プレゼンテーションの都合でデータアクセスから書き換えるってことだ。修正箇所が複数のレイヤーに分散されているのは作業効率がとても悪いと思う。


もう少し、プレゼンテーション層を作っている時にデータアクセス層の事を考えずに居られるようにしないとダメなんじゃないかと思う。データがアプリのメモリ上にあるのか設定ファイルにあるのかDBにあるのかとか、取り出すのに時間がかかるから事前にHashMapを作って、とか考えずに書けないとダメだ。


でそれを実現する方法だけど、データ操作をもう少し概念モデル寄りに記述出来るようにするってことと、その場合でもパフォーマンスが出るような仕組みにしておくってことが必要だ。やっぱりLINQ的なものが必要だと思う(LanguageにIntegrateされてるかはともかく。)