2011年1月26日水曜日

Custom XmlAdapter for Joda Time with xjc

JAXBを使ってますか?
JAXBとJoda-Timeを連携してみましょう。

以下のクラスとXMLをBindするのが目標です。
  1. public class Foo {  
  2.   private DateTime bar;  
  3.   private DateTime baz;  
  4. }  
  1. <foo>  
  2.   <bar>2011-2-3</bar>  
  3.   <baz>2010-3-4T05:06:07</baz>  
  4. </foo>  

JAXBでは面倒な点がいくつかありました。
1. @XmlType(propOrder)にフィールドを列挙する必要がある。
2. xs:dateはXmlGregorianCalendarに割り当てられている。

うーん、面倒ですね。GroovyだったらとかClojureだったらとか、思わずにはいられない。
それぞれの対策を書いていきます。

1. @XmlType(propOrder)にフィールド名を列挙する必要がある。
アノテーションでは順序が保たれないため、フィールドとpropOrderの双方に書く必要がある。
とりあえず生成するGroovyコードを書いたのですが、調べたらxjcなんてものがあったので、移植してみました。
かんたんなスキーマを書いてみます。
  1. <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">  
  2.   <xs:element name="foo">  
  3.     <xs:complextype>  
  4.       <xs:sequence>  
  5.         <xs:element name="bar" type="xs:date" />  
  6.         <xs:element name="baz" type="xs:dateTime" />  
  7.       </xs:sequence>  
  8.     </xs:complextype>  
  9.   </xs:element>  
  10. </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を使わない方法があったので参考にしました。
  1. <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">  
  2.   <xs:annotation><xs:appinfo>  
  3.     <jaxb:globalBindings>  
  4.       <jaxb:javaType name="java.util.Calendar" xmlType="xs:date"  
  5.         parseMethod="javax.xml.bind.DatatypeConverter.parseDate"  
  6.         printMethod="javax.xml.bind.DatatypeConverter.printDate"  
  7.         />  
  8.     </jaxb:globalBindings>  
  9.   </xs:appinfo></xs:annotation>  
  10. </xs:schema>  

なるほど。変換するメソッドを呼び出すことができるわけですね。
この例では準備されたメソッドですが、任意のメソッドを呼び出すことができます。
標準のはCalendarなので、java.util.Dateに変換するのを書けばいいだけ。
簡単ですね。
これでどんな型でも自由自在。
xs:appinfoを別のファイルに移し、xs:includeすると共通化できて便利です。
うまく名前空間を利用するとインクルードなしでもいけるんでしょうが。

ここで一つ別の問題が出てきました。

3. 余計なAdapterが生成される
Adapter1やAdapter2が勝手に生成されてしまいます。
できればこれらの名前も指定したいところ。
最終的にはこうなりました。
  1. <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"  
  2.   xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc" jaxb:extensionBindingPrefixes="xjc" jaxb:version="2.0">  
  3.   <xs:annotation>  
  4.     <xs:documentation>custom datetime adapter</xs:documentation>  
  5.     <xs:appinfo>  
  6.       <jaxb:globalBindings>  
  7.         <xjc:javaType name="org.joda.time.DateTime" adapter="hikoz.xml.DateAdapter" xmlType="xs:date"/>  
  8.         <xjc:javaType name="org.joda.time.DateTime" adapter="hikoz.xml.DateTimeAdapter" xmlType="xs:dateTime"/>  
  9.       </jaxb:globalBindings>  
  10.     </xs:appinfo>  
  11.   </xs:annotation>  
  12.   <xs:element name="foo">  
  13.     <xs:complexType>  
  14.       <xs:sequence>  
  15.         <xs:element name="bar" type="xs:date" />  
  16.         <xs:element name="baz" type="xs:dateTime" />  
  17.       </xs:sequence>  
  18.     </xs:complexType>  
  19.   </xs:element>  
  20. </xs:schema>  
Adapterはこんな感じ。
日付の形式も自由自在。ただしおすすめはできないけど。
読みたいこともあるよね。
  1. public class DateTimeAdapter extends XmlAdapter<String, Datetime> {  
  2.  private static final DateTimeFormatter YMDHMS = DateTimeFormat.forPattern("yyyy/MM/dd HH:mm:ss");  
  3.  @Override  
  4.  public DateTime unmarshal(String v) throws Exception {  
  5.   return YMDHMS.parseDateTime(v);  
  6.  }  
  7.   
  8.  @Override  
  9.  public String marshal(DateTime v) throws Exception {  
  10.   return YMDHMS.print(v);  
  11.  }  
  12. }  

以下はテストコードと、xjcの実行。
Antタスクもあったんだけど、最近build.xmlを書いてないから構文を忘れた。
Gradleかわいいよ

  1. @Test  
  2.   public void readwrite() throws Exception {  
  3.     StringWriter sw = new StringWriter();  
  4.     Foo foo = new Foo();  
  5.     foo.setBar(new DateTime(2011234567));  
  6.     foo.setBaz(new DateTime(2012345678));  
  7.     JAXB.marshal(foo, sw);  
  8.     StringReader sr = new StringReader(sw.toString());  
  9.     Foo foo2 = JAXB.unmarshal(sr, Foo.class);  
  10.     assertThat(foo2.getBar(), is(new DateTime(2011230000)));  
  11.     assertThat(foo2.getBaz(), is(new DateTime(2012345670)));  
  12.   }  
  13.   
  14.   public static void main(String[] args) throws Exception {  
  15.     com.sun.tools.xjc.Driver  
  16.         .main("-no-header -extension -d src/test/java -p hikoz.xml src/test/java/hikoz/xml/foo.xsd"  
  17.             .split("\\s+"));  
  18.   }