2008年7月6日日曜日

Java5+JAXBのメモ

TimeTrackingするためのSlimTimerのEclipsePluginが欲しい!という事で、まずは通信等のcoreな部分の機能をJavaで実装するところからはじめようかと作業し始めた。

  • http通信はcommons-httpclientを使う。特に何も考えずに決めた。
  • xmlでやりとりするので、JavaのModelとのObject-XMLのマッピングはJAXBを使ってみる。ただし、MacのJava6の導入方法がよくわかっていないのでJava5+jaxb-implでやる。
というカンジに作業を始めたので、主に「Java5+JAXB」的なpom.xmlと「既存のWEB上のAPIをJAXBでObjectへMapping」という実装手順についてメモっておく。状況によっては、JAXBが思ってたより便利ではないんじゃ?とか思ったのもある。

まずはpom.xml

java.netのリポジトリを見ると、JAXBの実装のバージョンは2.1だったのでそれを使用。commons-httpclientは4.0betaもあるが、慣れた3.1で。pom.xmlはこんなカンジ。
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.shin1o</groupId>
  <artifactId>com.shin1o.jslimtimer</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>com.shin1o.jslimtimer</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.4</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
      <version>1.1</version>
    </dependency>
    <dependency>
      <groupId>commons-httpclient</groupId>
      <artifactId>commons-httpclient</artifactId>
      <version>3.1-rc1</version>
    </dependency>
    <dependency>
      <groupId>javax.xml</groupId>
      <artifactId>jaxb-api</artifactId>
      <version>2.1</version>
    </dependency>
    <dependency>
      <groupId>javax.xml</groupId>
      <artifactId>jaxb-impl</artifactId>
      <version>2.1</version>
    </dependency>
  </dependencies>
  <repositories>
    <repository>
      <id>maven2-repository.dev.java.net</id>
      <name>Java.net Repository for Maven</name>
      <url>http://download.java.net/maven/2/</url>
      <layout>default</layout>
    </repository>
  </repositories>
</project>

SlimTimerのAPIを確認、JavaのObjectを作成する

SlimTimerのAPIのhelpを見てみる。一番最初の入り口の認証は以下のような仕様のようだ。
  • リクエストパラメータとしては「api-key」「userのid(email)」「userのpassword」の三種類。curlコマンドを使ってXMLでリクエストを送信するサンプルが掲載されている。こんなリクエストを送っている。
    <?xml version="1.0" encoding="UTF-8"?>
    <request>
      <user>
        <email>rrwhite@gmail.com</email>
        <password>testtest</password>
      </user>
      <api-key>94d641ad952e7e0</api-key>
    </request>
    説明を読んでみるとこれは「Content-Type: application/xml」を使ってPOSTした場合の話で、次のようなパラメータを使ってGETで取得する事もできるよぅだ。
    • api_key
    • user[email]
    • user[password]
  • 認証用APIのレスポンスのサンプルは以下のようになっている。
    <?xml version="1.0" encoding="UTF-8"?>
    <response>
      <user-id>1</user-id>
      <access-token>a74d823ecf63f481d92b1e52853fa4bcbe2239d1</access-token>
    </response>
  • これをそのままPOJO+JAXBのアノテーションで表現するとこんなカンジになるはず。
    package com.shin1o.slimtimer.model;
    
    import javax.xml.bind.annotation.XmlAccessType;
    import javax.xml.bind.annotation.XmlAccessorType;
    import javax.xml.bind.annotation.XmlElement;
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlRootElement(name = "response")
    public class STToken {
    
      @XmlElement(name = "user-id")
      private int userId;
    
      @XmlElement(name = "access-token")
      private String accessToken;
    
      public int getUserId() { return userId; }
      public void setUserId(int userId) { this.userId = userId; }
    
      public String getAccessToken() { return accessToken; }
      public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
    }
    @XmlAccessorType(XmlAccessType.FIELD)
    FieldをXMLとのマッピングに使うのか、アクセサをXMLとのマッピングに使うのか、等を設定する。
    @XmlRootElement(name = "response")
    POJOとマッピングするルート要素の要素名を設定する。
    @XmlElement(name = "user-id")
    Fieldとマッピングする要素の要素名を設定する。
    @XmlElementWrapper
    ここには出てこないが必須だと思われるので書いておく。例えば以下のようなXMLがあったとき。
    <root>
    <owners>
      <person>
        <name>Richard White</name>
      </person>
      <person>
        <name>Shin1 Ogawa</name>
      </person>
    </owners>
    </root>
    で、これをList<Person>で保持したいFieldは次のように修飾する。
    @XmlElementWrapper(name = "owners")
    @XmlElement(name = "person")
    List<Person> owners;
    @XmlElementWrapperを指定しない時は、ルート直下にperson要素が複数並ぶものにマッピングされる。

JavaのObjectからXmlSchemaを生成する

XmlSchemaはJAXBの実装のパッケージについてくるスクリプトで自動生成できるはずなので、それを使ってみる(XmlSchamaを手で書ける人間にはこれが罠だと後で気づくのだが)
  • JAXB2.1.7のツール群をDownloadする。
  • zipを展開し、${jaxb-ri}/bin/schemagenに実行属性を付加して自動生成させてみる。schemagen
    $ ./schemagen.sh -classpath ../../target/classes ../../src/main/java/com/shin1/slimtimer/model/STToken.java
  • 特にオプションもなく生成してみたので、${jaxb-ri}/bin直下に「schema1.xsd」というファイルが生成された。中身はこんなの。
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    
      <xs:element name="response" type="stToken"/>
    
      <xs:complexType name="stToken">
        <xs:sequence>
          <xs:element name="user-id" type="xs:int"/>
          <xs:element name="access-token" type="xs:string" minOccurs="0"/>
        </xs:sequence>
      </xs:complexType>
    </xs:schema>

XMLをPOJOにマッピングしてみる

SlimTimerにあった認証のレスポンスを使って、作成したPOJOにマッピングしてみる。ちなみに、XML->Objectの方向をUnmarshalと呼ぶ。
  • まずは生成したXmlSchema(XSDファイル)をsrc/main/resources/STToken.xsd等のCLASSPATHとして含まれている場所に移動&リネームしておく。
  • Testとして使用するxmlファイルも用意する。例えば、Test実行事にCLASSPATHに含まれるsrc/test/resources/testdatas/Token.xml等として配置したり。
    <?xml version="1.0" encoding="UTF-8"?>
    <response>
      <user-id>1</user-id>
      <access-token>a74d823ecf63f481d92b1e52853fa4bcbe2239d1</access-token>
    </response>
  • ソースは以下のようなカンジ。
    package com.shin1o.slimtimer.model;
    
    import static org.junit.Assert.assertEquals;
    
    import javax.xml.XMLConstants;
    import javax.xml.bind.JAXBContext;
    import javax.xml.bind.Unmarshaller;
    import javax.xml.validation.Schema;
    import javax.xml.validation.SchemaFactory;
    
    import org.junit.Test;
    
    public class STTokenTest {
      @Test
      public void unmarshal() throws Exception {
        SchemaFactory factory = SchemaFactory
            .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Schema schema = factory.newSchema(getClass().getClassLoader()
            .getResource("STToken.xsd"));
        JAXBContext context = JAXBContext.newInstance(STToken.class);
        Unmarshaller unmarshaller = context.createUnmarshaller();
        unmarshaller.setSchema(schema);
    
        STToken token = (STToken) unmarshaller.unmarshal(getClass()
            .getClassLoader().getResource("testdata/Token.xml"));
    
        assertEquals(1, token.getUserId());
        assertEquals("a74d823ecf63f481d92b1e52853fa4bcbe2239d1", token
            .getAccessToken());
      }
    }
    XmlSchemaをJavaから扱った経験がある人なら殆ど違和感は無いと思われる。
こんなカンジであっさり動作するワケだ。だが、こんなのHelloWorld的な話で、実際のWEBAPIを使い始めるとこんな簡単にはいかない。

せっかくだから実際に通信してみる

  • JUnit4のTestCaseとして実装する。
      private static String EMAIL = "xxxxx@xxx.com;
      private static String PASSWORD = "xxxxxxxxxx";
      private static String APIKEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    
      @Test
      public void getToken() throws Exception {
        HttpMethod method = new GetMethod();
        method.setURI(new HttpURL("www.slimtimer.com", 80, "/users/token"));
        method.addRequestHeader("Accept", "application/xml");
        method.setQueryString(new NameValuePair[] {
            new NameValuePair("api_key", APIKEY),
            new NameValuePair("user[email]", EMAIL),
            new NameValuePair("user[password]", PASSWORD) });
        HttpClient client = new HttpClient();
        client.executeMethod(method);
    
        // unmarshall
        JAXBContext context = JAXBContext.newInstance(STToken.class);
        Unmarshaller unmarshaller = context.createUnmarshaller();
    
        SchemaFactory factory = SchemaFactory
            .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Schema schema = factory.newSchema(TokenController.class
            .getClassLoader().getResource("STToken.xsd"));
        unmarshaller.setSchema(schema);
        String response = method.getResponseBodyAsString();
        System.out.println(response);
        STToken token = (STToken) unmarshaller.unmarshal(new StringReader(
            response));
    
        assertNotNull(token);
      }
  • 実行してみるとどーなるか?
    javax.xml.bind.UnmarshalException
     - with linked exception:
    [org.xml.sax.SAXParseException: cvc-complex-type.2.4.a: Invalid content was found starting with element 'access-token'. One of '{"":user-id}' is expected.]
      at javax.xml.bind.helpers.AbstractUnmarshallerImpl.createUnmarshalException(AbstractUnmarshallerImpl.java:315)
      ...
    やりましたね、UnmarshalExceptionです。user-id要素を期待したのにacess-token要素が来たぞ!と怒っています。
  • レスポンスを確認するとこんなカンジ。
    <?xml version="1.0" encoding="UTF-8"?>
    <response>
      <access-token>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</access-token>
      <user-id type="integer">00000</user-id>
    </response>
    サンプルと随分違う。まじめにHelp書いてくださいよ>SlimTimerの中の人。
    • access-tokenとuser-idの出現順序がサンプルとは違う。xsdではxs:sequenceとしてuser-id,access-tokenの順に定義している。これがExceptionの原因だ。
    • 後、まだExceptionとしては出ていないが、サンプルにはuser-id要素にtype属性なんてなかった。xsdではuse-id要素は属性を持たないとして定義している。
    これらの理由で、自動生成したxsdがそのままでは使えないのだ。
  • 仕方ないので、xsdを手動で修正する。sequenceをallとして、user-idはtype属性を持つように定義しなおす。
    <?xml version="1.0" encoding="UTF-8"?>
    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0">
      <xs:element name="response" type="stToken"/>
      
      <xs:complexType name="stToken">
        <xs:all>
          <xs:element name="user-id">
            <xs:complexType>
              <xs:simpleContent>
                <xs:extension base="xs:int">
                  <xs:attribute name="type" type="xs:string" use="optional"/>
                </xs:extension>
              </xs:simpleContent>
            </xs:complexType>
          </xs:element>
          <xs:element name="access-token" type="xs:string"/>
        </xs:all>
      </xs:complexType>
    </xs:schema>
  • これで@Test getToken()もGreenになった。ふぅ。。
  • 参考までに、修正後のxsdからPOJOを自動生成してみるとどんなカンジになるのかを見てみる。${jaxb-ri}/bin/xjcを使用するとxsdからPOJOの自動生成ができるので、それを使って自動生成してコメントを削除して小さくした物が以下。
    package generated;
    
    import javax.xml.bind.annotation.XmlAccessType;
    import javax.xml.bind.annotation.XmlAccessorType;
    import javax.xml.bind.annotation.XmlAttribute;
    import javax.xml.bind.annotation.XmlElement;
    import javax.xml.bind.annotation.XmlType;
    import javax.xml.bind.annotation.XmlValue;
    
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlType(name = "stToken", propOrder = {
    
    })
    public class StToken {
    
      @XmlElement(name = "user-id", required = true)
      protected StToken.UserId userId;
      @XmlElement(name = "access-token", required = true)
      protected String accessToken;
    
      public StToken.UserId getUserId() { return userId; }
      public void setUserId(StToken.UserId value) { this.userId = value; }
    
      public String getAccessToken() { return accessToken; }
      public void setAccessToken(String value) { this.accessToken = value; }
    
      @XmlAccessorType(XmlAccessType.FIELD)
      @XmlType(name = "", propOrder = { "value" })
      public static class UserId {
        @XmlValue
        protected int value;
        @XmlAttribute
        protected String type;
    
        public int getValue() { return value; }
        public void setValue(int value) { this.value = value; }
    
        public String getType() { return type; }
        public void setType(String value) { this.type = value; }
      }
    }
    たかがuser-idごときに内部classができちゃってます。typeなんてFieldは"integer"固定値で良さそうなのに。

...ずっとこんなカンジ!SlimTimerで使うたった数個のPOJOをサンプルのXMLをたよりにUnmarshalする、実際にWEBへアクセスしてそれをUnmarshalする、その辺りの実装中はずーーーっとxsdを修正するターン!Javaのターンなんてちっとも回ってこないのだ!SlimTimerのAPIの説明が不親切な点もある(もっと細かく書いてくれ。いや、そもそもXmlSchemaを提供してくれ!)し、JAXBの仕様が不親切な点もある(もっと柔軟に微調整させろ!)が、読み込み関連を全部実装した時点までに要した時間の90%以上は「xsdの調整」に費やされた。UnmarshallExceptionを飽きる程見た。途中で「DOM+XSLTでやった方が早い...」と何度も考えた。
そして、今後徐々にPOST/PUT/DELETE系のAPIを実装していくワケだが、その際に色々問題が考えられる。

  • timeentryのidとかってサーバ側で振られるから、POST時に存在したらアカンのでは?
  • APIHelp見てると、PUT時の「更新可能要素」みたいな事が書いてある。これ以外の要素をそのまま送っちゃうとアカンのでは?
等等、主にJAXBのMarshalを使う時の話。xsdのターンはもぅ来ないだろぅけど、JAXBの調整とSlimTimerの仕様をチマチマ確認して行くターンが来ると予想できる。JAXBの方はMarshalする時に独自のロジックを挟む事ができるはずだが、それくらいならStringBuilderを使う方が圧倒的にラクチンだという事が容易に想像できる。それでもJAXBを無理して使うのかどぅかは悩む。さらにSlimTimerの仕様をチマチマ確認するのは、非常に面倒そぅだ。なんかね、もぅ。><

コメントを投稿