真・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; } }
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クラスを増やして、文字列からの変換を実装するだけ)