StrutsのメッセージファイルをXMLにする

久々のJavaネタ。Strutsの話です。

Strutsではメッセージをpropertiesファイルに記述しますが、これを変更してXMLで書けるようにする方法を紹介します。

仕様

今回のサンプルでは、メッセージファイルはクラスパス直下に置くことにします。

言語化を考慮して、メッセージファイルの末尾にはロケールを付与するようにします。
(ファイル名の例)

message_ja.xml


XMLの形式はこんな感じで。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<messages>
    <message key="k0">
        <text>メッセージ0</text>
    </message>
    <message key="k1">
        <text>メッセージ1</text>
    </message>
    <message key="k2">
        <text>メッセージ2</text>
    </message>
</messages>

見たまんま。

クラスを作る

MessageResourcesとMessageResourcesFactoryを拡張したクラスを作ります。

まずはMessageResourcesの拡張。長いですが省略なしで。なおXMLのパースにはJAXBを使っています。

/**
 * 
 */
package aaa.bbb;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.xml.bind.JAXB;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

import org.apache.struts.util.MessageResources;
import org.apache.struts.util.MessageResourcesFactory;

/**
 * @author jabaraster
 */
public class XmlMessageResources extends MessageResources {
    private static final long                       serialVersionUID = -1390664385723178825L;

    private final Map<Locale, Map<String, Message>> messages         = new ConcurrentHashMap<Locale, Map<String, Message>>();

    /**
     * @param pFactory
     * @param pConfig
     */
    public XmlMessageResources(final MessageResourcesFactory pFactory, final String pConfig) {
        super(pFactory, pConfig);
    }

    /**
     * @see org.apache.struts.util.MessageResources#getMessage(java.util.Locale, java.lang.String)
     */
    @Override
    public String getMessage(final Locale pLocale, final String pMessageKey) {
        Map<String, Message> m = this.messages.get(pLocale);
        if (m == null) {
            m = load(pLocale, this.config);
            this.messages.put(pLocale, m);
        }
        final Message message = m.get(pMessageKey);
        if (message == null) {
            return "*** no message found for '" + pMessageKey + "' ***";
        }
        return message.getText();
    }

    private static void appendIfNotNull(final StringBuilder pBuffer, final String pToken) {
        if (pToken != null && pToken.trim().length() > 0) {
            pBuffer.append("_" + pToken);
        }
    }

    private static Map<String, Message> load(final Locale pLocale, final String pConfig) {
        try {
            final String path = resolvePath(pLocale, pConfig);
            final URL resourceLocation = XmlMessageResources.class.getResource(path);
            if (resourceLocation == null) {
                throw new IllegalStateException("メッセージリソース " + path + " が見付かりませんでした.");
            }
            final InputStream in = new BufferedInputStream(resourceLocation.openStream());
            final Messages messages = JAXB.unmarshal(in, Messages.class);
            final Map<String, Message> ret = new ConcurrentHashMap<String, XmlMessageResources.Message>();
            for (final Message msg : messages.getList()) {
                ret.put(msg.getKey(), msg);
            }
            return ret;

        } catch (final IOException e) {
            throw new IllegalStateException(e);
        }
    }

    private static String resolvePath(final Locale pLocale, final String pKey) {
        final StringBuilder buffer = new StringBuilder();
        buffer.append("/");
        buffer.append(pKey);
        appendIfNotNull(buffer, pLocale.getLanguage());
        appendIfNotNull(buffer, pLocale.getCountry());
        appendIfNotNull(buffer, pLocale.getVariant());
        buffer.append(".xml");
        return new String(buffer);
    }

    /**
     * @author jabaraster
     */
    @XmlRootElement
    private static class Message {
        private String key;
        private String text;

        /**
         * 
         */
        @SuppressWarnings("unused")
        public Message() {
            //
        }

        /**
         * @return the key
         */
        @XmlAttribute
        public String getKey() {
            return this.key;
        }

        /**
         * @return the text
         */
        public String getText() {
            return this.text;
        }

        /**
         * @param pKey the key to set
         */
        @SuppressWarnings("unused")
        public void setKey(final String pKey) {
            this.key = pKey;
        }

        /**
         * @param pText the text to set
         */
        @SuppressWarnings("unused")
        public void setText(final String pText) {
            this.text = pText;
        }
    }

    /**
     * @author jabaraster
     */
    @XmlRootElement
    private static class Messages {
        private List<Message> list = new ArrayList<XmlMessageResources.Message>();

        /**
         * @return the list
         */
        @XmlElement(name = "message")
        public List<Message> getList() {
            return this.list;
        }

        /**
         * @param pList the list to set
         */
        @SuppressWarnings("unused")
        public void setList(final List<Message> pList) {
            this.list = pList;
        }
    }
}

次にMessageResourceFactoryの拡張。さっき作ったXmlMessageResourcesを返すようにするだけです。

import org.apache.struts.util.MessageResources;
import org.apache.struts.util.MessageResourcesFactory;

/**
 * @author jabaraster
 */
public class XmlMessageResourcesFactory extends MessageResourcesFactory {
    private static final long serialVersionUID = -5520240862924828575L;

    /**
     * @see org.apache.struts.util.MessageResourcesFactory#createResources(java.lang.String)
     */
    @Override
    public MessageResources createResources(final String pConfig) {
        return new XmlMessageResources(this, pConfig);
    }
}

sturts-config.xmlを編集

XmlMessageResourcesFactoryをstruts-config.xmlに設定します。
今回は2つメッセージファイルを設定する例を挙げます。
(修正前)

<message-resources parameter="application"
    factory="org.seasar.struts.util.S2PropertyMessageResourcesFactory"/>
<message-resources parameter="label" key="label"
    factory="org.seasar.struts.util.S2PropertyMessageResourcesFactory"/>

(修正後)

<message-resources parameter="application"
    factory="aaa.bbb.XmlMessageResourcesFactory"/>
<message-resources parameter="label" key="label"
    factory="aaa.bbb.XmlMessageResourcesFactory"/>

メッセージファイルを作る

application_ja.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<messages>
    <message key="k0">
        <text>メッセージ0</text>
    </message>
    <message key="k1">
        <text>メッセージ1</text>
    </message>
    <message key="k2">
        <text>メッセージ2</text>
    </message>
</messages>

label_ja.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<messages>
    <message key="k3">
        <text>メッセージ3</text>
    </message>
    <message key="k4">
        <text>メッセージ4</text>
    </message>
    <message key="k5">
        <text>メッセージ5</text>
    </message>
</messages>

使う

準備は完了です。

JSPでメッセージを表示してみます。

<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head>
	<title>XMLでメッセージ</title>
	<meta charset="UTF-8">
</head>
<body style="line-height: 1.5em;">

  <bean:message key="k0"/> <br/>
  <bean:message key="k3" bundle="label"/>
	
</body>
</html>

実際に表示してみます。

ちゃんとメッセージが取得出来ました。

XMLにする意義

propertiesファイルはnative2asciiでUnicodeエスケープしないと使えません。
これはかなり不便です。

Unicodeエスケープ不要なフォーマットとしてはXMLJSONYAMLなど無数に考えられます。
その中にあって、XML

  • 別途ライブラリを追加する必要がない(Java標準で付属している)
  • JAXBという便利なツールがある

という点で有利です。

SAStrutsを使う場合の考慮点

上記サンプルはSAStrutsのHOT deployに対応していません。
SAStrutsを使う場合はご注意を。