plistっぽいxml読み込み用digester-rules

(2008/5/24 追記: 本物のplist読み込みについては真・plist形式のxml読み込み用digester-rules(およびクラス) - terazzoの日記を参照)

設定ファイルをXMLで書く事って多いんだけど、javaXML使う時って型安全や妥当性に拘りすぎて仰々しいと時々思う。かといってPropertiesだと階層的なデータは表現し辛い。JSONYAMLObjective-Cのplistみたいに手軽に階層的なデータが使えると嬉しいと思う。


ということでCommons Digester(初めて使った)でそれっぽいものを書いてみた。解説などは後ろで。

使用例

読み込むXMLファイル。レポートメールの設定ファイルのイメージで。

<?xml version = "1.0" encoding = "UTF-8" ?>	
<map>
	<string key="smtpHost">mx010</string>
	<integer key="smtpPort">25</integer>
	<string key="fromAddress">terazzo@example.com</string>
	<string key="subject">Report Mail</string>
	<list key="bccAddresses">
		<string>admin1@example.com</string>
		<string>admin2@example.com</string>
		<string>admin3@example.com</string>
	</list>
	<string key="templatePath">mail_template/ReportMail.html</string>
</map>

読み込みプログラム

        String configPath = "config/sample.ReportMailer";
        Map map = new Configuration(new File(configPath).toURL());
        System.out.println("" + map);

出力結果(途中で適宜改行している)

{
    templatePath=mail_template/ReportMail.html,
    smtpHost=mx010, 
    smtpPort=25,
    fromAddress=terazzo@example.com,
    bccAddresses=[admin1@example.com, admin2@example.com, admin3@example.com],
     subject=Report Mail
}

ソースコード

ルールファイル。もう少し簡単にかけそうだけど……

src/sample/config/collections-rule.xml:

<?xml version = "1.0" encoding = "UTF-8" ?>    
<digester-rules>
  <object-create-rule pattern="map" classname="java.util.HashMap"/>
  <pattern value="*/map/string">
      <call-method-rule methodname="put" paramcount="2" paramtypes="java.lang.String,java.lang.String"/>
      <call-param-rule paramnumber="0" attrname="key"/>
      <call-param-rule paramnumber="1"/>
   </pattern>
  <pattern value="*/map/integer">
      <call-method-rule methodname="put" paramcount="2" paramtypes="java.lang.String,java.lang.Integer"/>
      <call-param-rule paramnumber="0" attrname="key"/>
      <call-param-rule paramnumber="1"/>
   </pattern>
  <pattern value="*/map/map">
      <object-create-rule classname="java.util.HashMap"/>
      <call-method-rule targetoffset="1" methodname="put" paramcount="2" />
      <call-param-rule paramnumber="0" attrname="key"/>
      <call-param-rule paramnumber="1" from-stack="true"/>
   </pattern>
  <pattern value="*/map/list">
      <object-create-rule classname="java.util.ArrayList"/>
      <call-method-rule targetoffset="1" methodname="put" paramcount="2" />
      <call-param-rule paramnumber="0" attrname="key"/>
      <call-param-rule paramnumber="1" from-stack="true"/>
   </pattern>
  <pattern value="*/list/string">
      <call-method-rule methodname="add" paramcount="1" paramtypes="java.lang.String"/>
      <call-param-rule  paramnumber="0" />
   </pattern>
  <pattern value="*/list/integer">
      <call-method-rule methodname="add" paramcount="1" paramtypes="java.lang.Integer"/>
      <call-param-rule  paramnumber="0" />
   </pattern>
  <pattern value="*/list/map">
      <object-create-rule classname="java.util.HashMap"/>
      <call-method-rule targetoffset="1" methodname="add" paramcount="1"/>
      <call-param-rule  paramnumber="0" from-stack="true" />
   </pattern>
  <pattern value="*/list/list">
      <object-create-rule classname="java.util.ArrayList"/>
      <call-method-rule targetoffset="1" methodname="add" paramcount="1"/>
      <call-param-rule  paramnumber="0" from-stack="true" />
   </pattern>

</digester-rules>


読み込み用クラス

package sample.config;

import java.net.URL;
import java.util.Collection;
import java.util.Map;
import java.util.Set;

import org.apache.commons.digester.Digester;
import org.apache.commons.digester.xmlrules.DigesterLoader;

/**
 * collections-rule.xmlを使ったDigesterによって設定ファイルを読み込むクラス
 */
public class Configuration implements Map {
    
    private static final String RULE_XML_NAME = "collections-rule.xml";

    /**
     * 設定ファイル読み込み用のDigester
     */
    private static Digester digester =
        DigesterLoader.createDigester(Configuration.class.getResource(RULE_XML_NAME));
    
    private URL url;
    private Map map;

    public Configuration(URL url) {
        super();
        this.url = url;
    }
    private synchronized Map getMap() {
        if (needsReloading()) {
            this.map = load(url);
        }
        return this.map;
    }
    /**
     * urlからdigesterを使用してMapを読み込む
     * @param url URL
     * @return urlから読み込んだMap
     */
    protected Map load(URL url) {
        try {
            return (Map)digester.parse(url);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("Exception occurs while loading configuration.", e);
        }
    }
    /**
     * @return 再読み込みが必要ならtrueを戻す
     */
    protected boolean needsReloading() {
        return this.map == null;
    }
    /**
     * mapのtoString()の結果を戻す
     */
    public String toString() {
        return getMap().toString();
    }
    /**
     * 
     * @see java.util.Map#clear()
     */
    public void clear() {
        getMap().clear();
    }
   ... 以下委譲メソッドが続く
}

環境

  • J2SE 5.0
  • commons-digester-1.8.jar
  • commons-logging-1.0.4.jar
  • commons-beanutils-core-1.7.0.jar

解説

  • map/listが再帰的に使える。mapはHashMap、listはArrayListに変換される。
  • ルートエレメントはmap
  • map/list内にstring, integerを含める事が出来る
  • map内のエレメントは、key=で指定された文字列をキーにHashMapにセットされる
  • ConfigurationクラスはMapインタフェースを実装。但し読み込んだHashMapに委譲しているだけ。

その他

Commons Configurationを使いたかったけど、キーに対する値をMapで欲しい時って結構多いので微妙だ。(config.getMap("key")が欲しいのよ。)


あんまりMapをベタに使うのってjava wayじゃないよね。でもruleを毎回書かなくてよい楽さは異常なので重宝しますよ。


とは言えせめてキーぐらいはリテラルで書かずに使えるようにしたい。ということでその辺は次回。