JAXBとJoda-Timeを連携してみましょう。
以下のクラスとXMLをBindするのが目標です。
public class Foo {
private DateTime bar;
private DateTime baz;
}
<foo> <bar>2011-2-3</bar> <baz>2010-3-4T05:06:07</baz> </foo>
JAXBでは面倒な点がいくつかありました。
1. @XmlType(propOrder)にフィールドを列挙する必要がある。
2. xs:dateはXmlGregorianCalendarに割り当てられている。
うーん、面倒ですね。GroovyだったらとかClojureだったらとか、思わずにはいられない。
それぞれの対策を書いていきます。
1. @XmlType(propOrder)にフィールド名を列挙する必要がある。
アノテーションでは順序が保たれないため、フィールドとpropOrderの双方に書く必要がある。
とりあえず生成するGroovyコードを書いたのですが、調べたらxjcなんてものがあったので、移植してみました。
かんたんなスキーマを書いてみます。
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="foo">
<xs:complextype>
<xs:sequence>
<xs:element name="bar" type="xs:date" />
<xs:element name="baz" type="xs:dateTime" />
</xs:sequence>
</xs:complextype>
</xs:element>
</xs:schema>
生成されたbar,bazの型は、見慣れないXmlGregorianCalendar なんだこれ。2. xs:dateはXmlGregorianCalendarに割り当てられている。
http://weblogs.java.net/blog/kohsuke/archive/2006/03/how_do_i_map_xs.html
xjcの開発者であるkohsukeのブログでXmlGregorianCalendarを使わない方法があったので参考にしました。
<xs:schema elementFormDefault="qualified" version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:jaxb="http://java.sun.com/xml/ns/jaxb" jaxb:version="2.0" targetNamespace="calendar-schemalet">
<xs:annotation><xs:appinfo>
<jaxb:globalBindings>
<jaxb:javaType name="java.util.Calendar" xmlType="xs:date"
parseMethod="javax.xml.bind.DatatypeConverter.parseDate"
printMethod="javax.xml.bind.DatatypeConverter.printDate"
/>
</jaxb:globalBindings>
</xs:appinfo></xs:annotation>
</xs:schema>
なるほど。変換するメソッドを呼び出すことができるわけですね。
この例では準備されたメソッドですが、任意のメソッドを呼び出すことができます。
標準のはCalendarなので、java.util.Dateに変換するのを書けばいいだけ。
簡単ですね。
これでどんな型でも自由自在。
xs:appinfoを別のファイルに移し、xs:includeすると共通化できて便利です。
うまく名前空間を利用するとインクルードなしでもいけるんでしょうが。
ここで一つ別の問題が出てきました。
3. 余計なAdapterが生成される
Adapter1やAdapter2が勝手に生成されてしまいます。
できればこれらの名前も指定したいところ。
最終的にはこうなりました。
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc" jaxb:extensionBindingPrefixes="xjc" jaxb:version="2.0">
<xs:annotation>
<xs:documentation>custom datetime adapter</xs:documentation>
<xs:appinfo>
<jaxb:globalBindings>
<xjc:javaType name="org.joda.time.DateTime" adapter="hikoz.xml.DateAdapter" xmlType="xs:date"/>
<xjc:javaType name="org.joda.time.DateTime" adapter="hikoz.xml.DateTimeAdapter" xmlType="xs:dateTime"/>
</jaxb:globalBindings>
</xs:appinfo>
</xs:annotation>
<xs:element name="foo">
<xs:complexType>
<xs:sequence>
<xs:element name="bar" type="xs:date" />
<xs:element name="baz" type="xs:dateTime" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
Adapterはこんな感じ。日付の形式も自由自在。ただしおすすめはできないけど。
読みたいこともあるよね。
public class DateTimeAdapter extends XmlAdapter<String, Datetime> {
private static final DateTimeFormatter YMDHMS = DateTimeFormat.forPattern("yyyy/MM/dd HH:mm:ss");
@Override
public DateTime unmarshal(String v) throws Exception {
return YMDHMS.parseDateTime(v);
}
@Override
public String marshal(DateTime v) throws Exception {
return YMDHMS.print(v);
}
}
以下はテストコードと、xjcの実行。
Antタスクもあったんだけど、最近build.xmlを書いてないから構文を忘れた。
Gradleかわいいよ
@Test
public void readwrite() throws Exception {
StringWriter sw = new StringWriter();
Foo foo = new Foo();
foo.setBar(new DateTime(2011, 2, 3, 4, 5, 6, 7));
foo.setBaz(new DateTime(2012, 3, 4, 5, 6, 7, 8));
JAXB.marshal(foo, sw);
StringReader sr = new StringReader(sw.toString());
Foo foo2 = JAXB.unmarshal(sr, Foo.class);
assertThat(foo2.getBar(), is(new DateTime(2011, 2, 3, 0, 0, 0, 0)));
assertThat(foo2.getBaz(), is(new DateTime(2012, 3, 4, 5, 6, 7, 0)));
}
public static void main(String[] args) throws Exception {
com.sun.tools.xjc.Driver
.main("-no-header -extension -d src/test/java -p hikoz.xml src/test/java/hikoz/xml/foo.xsd"
.split("\\s+"));
}