真・plist形式のxml読み込み用digester-rules(およびクラス)

iTunesでライブラリとして読み込まれた楽曲の情報は、所定のディレクトリに「iTunes Music Library.xml」というファイル名で保存される。今回、このファイルの内容を読み込む必要があったので、読み込み用のプログラムを書いてみた。

iTunes Music Library.xmlの形式

iTunes Music Library.xmlは、次のようなXML、いわゆるXML Property Lists形式になっている

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Major Version</key><integer>1</integer>
    <key>Minor Version</key><integer>1</integer>
    <key>Application Version</key><string>5.0</string>
    <key>Music Folder</key><string>file://localhost/C:/Documents%20and%20Settings/USER/My%20Documents/My%20Music/iTunes/iTunes%20Music/</string>
    <key>Library Persistent ID</key><string>D2041682111B43B4</string>
    <key>Tracks</key>
    <dict>
     <key>76</key>
     <dict>
         <key>Track ID</key><integer>76</integer>
         <key>Name</key><string>Impronta</string>
         <key>Artist</key><string>霜月はるか</string>
         <key>Composer</key><string>霜月はるか</string>
         <key>Album</key><string>Maple Leaf Box</string>
         <key>Genre</key><string>Soundtrack</string>
         <key>Kind</key><string>WAV オーディオファイル</string>
         <key>Size</key><integer>40687292</integer>
         <key>Total Time</key><integer>230653</integer>
         <key>Disc Number</key><integer>1</integer>
         <key>Disc Count</key><integer>1</integer>
         <key>Track Number</key><integer>1</integer>
         <key>Track Count</key><integer>12</integer>
         <key>Year</key><integer>2006</integer>
         <key>Date Modified</key><date>2008-02-26T14:54:46Z</date>
         <key>Date Added</key><date>2008-02-26T14:54:24Z</date>
         <key>Bit Rate</key><integer>1411</integer>
         <key>Sample Rate</key><integer>44100</integer>
         <key>Track Type</key><string>File</string>
         <key>Location</key><string>file://localhost/C:/Documents%20and%20Settings/USER/My%20Documents/My%20Music/iTunes/iTunes%20Music/%E9%9C%9C%E6%9C%88%E3%81%AF%E3%82%8B%E3%81%8B/Maple%20Leaf%20Box/01%20Impronta.wav</string>
         <key>File Folder Count</key><integer>4</integer>
         <key>Library Folder Count</key><integer>1</integer>
     </dict>
     <key>77</key>
     <dict>
...

見れば分かるように、文字列・数値・日付・配列・連想配列などが意味ではなくてデータ型に基づいてタグ付けされている。XMLの使い方としてはあまり一般的ではないかも。

方針

  • 意味に踏み込まず、基本的な値クラス+コレクションとして読み込む
    • ,,をString,Integer,java.util.Dateとして読み込む
    • をArrayList, をHashMapとして読み込む。はStringで固定
    • ,,,は今回は対応しない
  • 内部はとその値が交互に来る。直接Mapを生成してcall-method-ruleでputするのは難しそう
    • 一時的にラッパーを生成してキーを保持し、上位(外側)のコレクションにaddする時に中身(Map)を取り出すようにする
    • にもラッパーを使用

ソースコード

まずdigester-rulesファイル。keyはdict内にしか出て来ないのでDictクラスのsetKeyを呼ぶ。それ以外はインスタンスを生成して上位のコレクションにadd。
plist-rule.xml:

<?xml version = "1.0" encoding = "UTF-8" ?>    
<digester-rules>
  <object-create-rule pattern="plist" classname="sample.plist.PropertyList"/>
  <pattern value="*/key">
    <call-method-rule methodname="setKey" paramcount="1" paramtypes="java.lang.String"/>
    <call-param-rule paramnumber='0'/>
  </pattern>
  <pattern value="*/string">
    <call-method-rule methodname="add" paramcount="1" paramtypes="java.lang.String"/>
    <call-param-rule paramnumber='0'/>
    </pattern>
  <pattern value="*/integer">
    <call-method-rule methodname="add" paramcount="1" paramtypes="java.lang.Integer"/>
    <call-param-rule paramnumber='0'/>
    </pattern>
  <pattern value="*/date">
    <object-create-rule classname="sample.plist.wrapper.Date"/>
    <call-method-rule methodname="takeDateString" paramcount="1" paramtypes="java.lang.String"/>
    <call-param-rule paramnumber='0'/>
    <call-method-rule targetoffset="1" methodname="add" paramcount="1"
        paramtypes="sample.plist.ValueWrapper"/>
    <call-param-rule paramnumber='0'  from-stack="true"/>
    </pattern>
  <pattern value="*/dict">
    <object-create-rule classname="sample.plist.wrapper.Dict"/>
    <call-method-rule targetoffset="1" methodname="add" paramcount="1" 
         paramtypes="sample.plist.ValueWrapper"/>
    <call-param-rule paramnumber='0'  from-stack="true"/>
    </pattern>
  <pattern value="*/array">
    <object-create-rule classname="sample.plist.wrapper.Array"/>
    <call-method-rule targetoffset="1" methodname="add" paramcount="1" 
         paramtypes="sample.plist.ValueWrapper"/>
    <call-param-rule paramnumber='0'  from-stack="true"/>
    </pattern>
</digester-rules>


Digesterを使用してplistの中身の読み込みをおこなうクラス

package sample.plist;

import java.io.IOException;
import java.net.URL;

import org.apache.commons.digester.Digester;
import org.apache.commons.digester.xmlrules.DigesterLoader;
import org.xml.sax.SAXException;

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

    /**
     * 設定ファイル読み込み用のDigester
     */
    private static Digester digester =
        DigesterLoader.createDigester(PropertyListUtils.class.getResource(RULE_XML_NAME));
    
    /**
     * 指定したurlの内容をplistとして読み込み、内部の情報を戻す。
     * 内部のタグについて、arrayはjava.util.Listに、dictはjava.util.Mapに、
     * string, integer, dateはそれぞれString, Integer, java.util.Dateに変換する。
     * @param url XML形式のplistファイルを示すURL
     * @return plistの中身を戻す
     */
    public static Object load(URL url) throws IOException, SAXException {
        return ((PropertyList) digester.parse(url)).getContent();
    }
}


ラッパー用のインタフェース

package sample.plist;

/**
 * digesterでタグを処理する際に一時的に値を保持するラッパーインタフェース
 * @author terazzo
 */
public interface ValueWrapper {
    /** @return 内部に保持する値を戻す */
    Object getValue();
}


コレクション用の抽象スーパークラス。ValueWrapperをaddする際に代わりにその内容を使用する。

package sample.plist;

/**
 * ValueWrapperを受け取りValueWrapperの内部の値を取得するクラス
 * @author terazzo
 */
public abstract class Peeler {
    /**
     * valueを追加する
     * @param value 追加する値
     */    
    public abstract void add(Object value);
    /**
     * wrapperの代わりにwrapperが内部に保持する値を取り出して追加する
     * @param wrapper 追加する値を含むValueWrapper
     */    
    public void add(ValueWrapper wrapper) {
        add(wrapper.getValue());
    }
}


を読み込む為のラッパークラス

package sample.plist.wrapper;

import java.util.HashMap;
import java.util.Map;

import sample.plist.Peeler;
import sample.plist.ValueWrapper;

/**
 * dictタグを読み込む為のラッパークラス
 * @author terazzo
 */
public class Dict extends Peeler implements ValueWrapper {
    
    /** 内部ではMapで保持*/
    private Map map = new HashMap();
    
    /** 最後に出現したkeyタグの値を保持 */
    private String key;

    /**
     * keyを設定する。この直後にaddした値をこのkeyを用いて内部のMapに追加する
     * @param key key文字列
     */    
    public void setKey(String key) {
        this.key = key;
    }
    /**
     * valueをmapに追加する。最後に設定したkey値をキーとして使用する
     * @param value 追加する値
     */    
    public void add(Object value) {
        this.map.put(this.key, value);
    }
    /** @return 内部に保持するMap値を戻す */
    public Object getValue() {
        return this.map;
    }
}


を読み込む為のラッパークラス

package sample.plist.wrapper;

import java.util.ArrayList;
import java.util.List;

import sample.plist.Peeler;
import sample.plist.ValueWrapper;

/**
 * arrayタグを読み込む為のラッパークラス
 * @author terazzo
 */
public class Array extends Peeler implements ValueWrapper {
    
    /** 内部ではListで保持*/
    private List list = new ArrayList();

    /**
     * valueをlistに追加する
     * @param value 追加する値
     */    
    public void add(Object value) {
        this.list.add(value);
    }
    /** @return 内部に保持するList値を戻す */
    public Object getValue() {
        return this.list;
    }
}


を読み込む為のラッパークラス。デフォルトコンストラクタで生成してtakeDateString()で値を設定。

package sample.plist.wrapper;

import java.text.ParseException;
import java.text.SimpleDateFormat;

import sample.plist.ValueWrapper;

/**
 * dateタグを読み込む為のラッパークラス
 * @author terazzo
 */
public class Date implements ValueWrapper {
    
    /** 日付フォーマット(ISO 8601。実際は"yyyy-MM-dd'T'HH:mm:ss'Z'"で固定)。 */
    private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";

    /** 内部的に保持する日付オブジェクト*/
    private java.util.Date date = new java.util.Date();

    /**
     * 文字列dateStringを日付として設定する
     * @param dateString 日付を表す文字列(フォーマットは"yyyy-MM-dd'T'HH:mm:ss'Z'")
     */
    public void takeDateString(String dateString) throws ParseException {
        this.date = new SimpleDateFormat(DATE_FORMAT).parse(dateString);
    }
    /** @return 内部に保持するDate値を戻す */
    public Object getValue() {
        return this.date;
    }
}


用のクラス

package sample.plist;

/**
 * plistタグを読み込む為のラッパークラス
 * @author terazzo
 */
public class PropertyList extends Peeler {

    /** 内部ではObjectで保持*/
    private Object content;
    /**
     * 値を設定する
     * @param value plistタグ内の値(Map/List/String/Integerなど)
     */
    public void add(Object value) {
        this.content = value;
    }
    /** @return 内部に保持する値を戻す */
    public Object getContent() {
        return this.content;
    }
}

使用例

楽曲情報(Tracks)を内容をプリントしてみた

    private static final String TEST_FILE_PATH = "iTunes Music Library.xml";
    public void printTracks() throws IOException, SAXException {
        URL url = getClass().getResource(TEST_FILE_PATH);
        Object content = PropertyListUtils.load(url);
        Map<String, Map> tracks = (Map<String, Map>)((Map) content).get("Tracks");
        for (Map track : tracks.values()) {
            Integer trackNo = (Integer) track.get("Track Number");
            String genre = (String) track.get("Genre");
            String album = (String) track.get("Album");
            String artist = (String) track.get("Artist");
            String name = (String) track.get("Name");
            String location = (String) track.get("Location");
            System.out.println("------------------------------");
            System.out.println("  name: " + name);
            System.out.println("  album: " + album);
            System.out.println("  artist: " + artist);
            System.out.println("  trackNo: " + trackNo);
            System.out.println("  genre: " + genre);
            System.out.println("  filename: " + 
                    URLDecoder.decode(location.substring(1 + location.lastIndexOf("/")), "UTF-8"));
        }
   }


実行結果

------------------------------
  name: 9:02pm
  album: THE IDOLM@STER MASTER BOX ENCORE I&II DISC-2
  artist: 水瀬伊織 (釘宮理恵)
  trackNo: 19
  genre: Soundtrack
  filename: 2-19 9_02pm.wav
------------------------------
  name: 花祭りの娘
  album: ティンダーリアの種
  artist: 霜月はるか
  trackNo: 5
  genre: Soundtrack
  filename: 05 花祭りの娘.wav
------------------------------
...

順番はともかくちゃんと読めてるっぽい

環境

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

感想その他

  • 折角digester使ってもキャストとリテラルの嵐になるなあ。やっぱりJavaっぽくはない。
  • DOMとして取得してDOM API経由で中身にアクセスしてもあまり手間が変わらない気もする。(毎回型変換書かないで済むぐらい?)
  • realとかdataに対応するのはそんなに難しく無そう。(対応するwrapperクラスを増やして、文字列からの変換を実装するだけ)