2011年1月26日水曜日

Custom XmlAdapter for Joda Time with xjc

JAXBを使ってますか?
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+"));
  }

0 件のコメント: