How to create REST client in Java (2)

REST

第一回に引き続き Java で REST クライアントを作成していきます。

はじめに

【第一回】では Jerseryを利用してJavaでGETリクエストを送信し、この結果を文字列として取得するところまで書きましたが、今回はこれをJavaオブジェクトにマッピングするための方法を書きます。

Jersey では、Webサービスから返された XML や JSON 形式のデータを Java オブジェクトにマッピング ( ≒ 変換 ) する事が可能です。

マッピングには JAXB を利用します。

実装の流れ

何らかのWebサービスを利用するために Java で REST クライアントを実装するとします。
GETでリソース取得を行い、これに対する何らかの処理を行う場合、実装の流れは以下のようになります。

  1. 利用するWebサービスの仕様を確認
    • リソースURI
    • 設定が必要なリクエストパラメータ
    • 設定が必要なHTTPヘッダ
    • リソースのデータ仕様
  2. リソースのデータに対応する Java クラス の生成
  3. GETリクエストの結果をJavaクラスにマッピング (バインディング) するコードの生成
  4. 生成したコードを利用して何らかの処理を実施

今回、Magnolia CMS の REST API仕様を例に取って実装してみます。

仕様の確認

Magnolia CMS の REST API 仕様に関しては REST API - Magnolia、及び、REST webservices - Magnolia に記載があります。

リソースURI

REST エンドポイント (=URI) は以下のようになっています。
http://localhost:8080/magnoliaAuthor/.rest/nodes/v1/website/demo-project/about

Tomcatバンドル版を利用してローカルPC上で実行している場合、上記URIにGETリクエストを送信する事でリソースが取得できます。

Magnolia CMS には デモサイト が存在するのですが、試しにここに対して以下のURIで GETリスエストを送信してみたら、リソースが取得できました ( 前回作成した RestClientクラスを利用してXML/JSON形式のリソースが取得できる事を確認 )。
http://demo.magnolia-cms.com/.rest/nodes/v1/website/demo-project

以下取得のために利用したコード ( 抜粋 )

    public static void main(String[] args) {
        RestClient client = new RestClient("superuser","superuser");
        String uri = "http://demo.magnolia-cms.com/.rest/nodes/v1/website/demo-project";

        String xml = client.getString(uri, MediaType.APPLICATION_XML_TYPE);
        System.out.println(xml);
    }

必要なリクエストパラメータ

特にありません。

必要なHTTPヘッダ

処理内容によっては認証を行う必要があります。前回も書いたように基本認証を利用します。

リソースのデータ仕様

REST webservices - Magnolia に XML format、及び、JSON format が記載されています。

XML Schema がないか探してみたのですが、見つける事ができていません。

上記デモサイトのdemo-poject のリソースを取得した結果は以下のようになりました。 ( 実際のデータは改行、及び、インデントはされていません )

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<node>
  <identifier>1be12547-ad82-4c83-8396-213466ceb003</identifier>
  <name>demo-project</name>
  <nodes/>
  <path>/demo-project</path>
  <properties>
    <property>
      <multiple>false</multiple>
      <name>logoImg</name>
      <type>String</type>
      <values>
        <value>jcr:a1918662-8cbe-4346-ac5a-1b24a9950e2b</value>
      </values>
  </property>
  <property>
    <multiple>false</multiple>
    <name>title</name>
    <type>String</type>
    <values>
      <value>Home</value>
    </values>
  </property>
  <property>
    <multiple>false</multiple>
    <name>searchUUID</name>
    <type>String</type>
    <values>
      <value>e301b5fc-275d-440a-8e98-17cba32c29c3</value>
    </values>
  </property>
  <property>
    <multiple>false</multiple>
    <name>hideInNav</name>
    <type>Boolean</type>
    <values>
      <value>false</value>
    </values>
  </property>
  <property>
    <multiple>false</multiple>
    <name>printLogoImg</name>
    <type>String</type>
    <values>
      <value>jcr:7240d7f5-4d10-4f56-a944-7864cfb77f5b</value>
    </values>
  </property>
  <property>
    <multiple>false</multiple>
    <name>siteTitle</name>
    <type>String</type>
    <values>
      <value>Demo Project</value>
    </values>
  </property>
  </properties>
  <type>mgnl:page</type>
</node>

リソースに対応するクラスの作成

XML Schema 等のXML定義がある場合、jdk に含まれる xjc コマンドを利用して対応するクラスを作成する事が可能です。この場合はコマンドラインで以下のようにするだけです。

C:\> xjc <スキーマファイル名>

今回はXML Schema を見つける事ができなかったため、上記 XML format に従って対応するクラスを作成します。
定義を見たところ、node、及び、property 要素に対応したクラスを作成すればよさそうです。

Nodeクラスの作成

node 要素には以下の子要素が含まれています。

要素名内容
identifierコンテンツのid
nameコンテンツ名
typemgnl:page 等コンテンツの種類
pathコンテンツのパス
propertiesプロパティ。別途Propertyクラスで定義する
children子ノード。Node 自体を再帰的に格納可能。

これを元に Node クラスを生成します。

Node.java

package jp.co.agilegroup.magnolia.rest.dto;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;

/**
 * Node
 * @author masao.suda
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "node")
public class Node implements Serializable {

    /** serialVersionUID. */
    private static final long serialVersionUID = 1L;

    @XmlElement
    public String identifier;

    /** name. */
    @XmlElement(required = true)
    public String name;
    
    /** type. */
    @XmlElement(required = true)
    public String type;

    /** path. */
    @XmlElement(required = true)
    public String path;
    
    /** properties. */
    @XmlElement(required = true, name = "property")
    @XmlElementWrapper
    private List<Property> properties;

    /** children. */
    @XmlElement(name = "child")
    @XmlElementWrapper
    private List<Node> children;
    
    public List<Property> getProperties() {
        if (this.properties == null) {
            this.properties = new ArrayList<Property>();
        }
        return properties;
    }
    public void setProperties(List<Property> propertyList) {
        this.properties = propertyList;
    }
    public void addProperty(Property property) {
        if (this.properties == null) {
            this.properties = new ArrayList<Property>();
        }
        this.properties.add(property);
    }
    public void addProperty(String name, String type, boolean multiple, String value) {
        if (this.properties == null) {
            this.properties = new ArrayList<Property>();
        }
        Property property = new Property();
        property.name = name;
        property.type = type;
        property.multiple = multiple;
        property.addValue(value);
        this.properties.add(property);
    }
    
    public List<Node> getChildren() {
        if (children == null) {
            children = new ArrayList<Node>();
        }
        return children;
    }
    public void setChildren(List<Node> childList) {
        this.children = childList;
    }
    public void addChild(Node child) {
        if (this.children == null) {
            this.children = new ArrayList<Node>();
        }
        this.children.add(child);
    }
}

Propertyクラスの作成

Property.java も作成します。

Property.java

要素名内容
nameプロパティ名
typeString等プロパティの種類(型)
multiple値が複数かをしめすboolean値
values
package jp.co.agilegroup.magnolia.rest.dto;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;

/**
 * Property
 * @author masao.suda
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "property")
public class Property implements Serializable {

    /** serialVersionUID. */
    private static final long serialVersionUID = 1L;

    /** name. */
    @XmlElement(required = true)
    public String name;
    
    /** type. */
    @XmlElement(required = true)
    public String type;
    
    /** multiple. */
    @XmlElement(required = true)
    public Boolean multiple = false;
    
    /** values. */
    @XmlElement(required = true, name = "value")
    @XmlElementWrapper
    private List<String> values;

    public List<String> getValues() {
        if (this.values == null) {
            this.values = new ArrayList<String>();
        }
        return values;
    }
    public void setValues(List<String> values) {
        this.values = values;
    }
    public void addValue(String value) {
        if (this.values == null) {
            this.values = new ArrayList<String>();
        }
        this.values.add(value);
    }
}

リソースに対応するクラスは、所謂POJOにJAXBのアノテーションを付与するかたちで作成します。
作成する手順は以下のような感じでしょうか。

  1. リソースに対応するクラスを作成
    上ではリソース(XML)のルート要素に対応する名前のクラスを作成しています。
  2. リソースの要素に対応するフィールドを作成。
    上記ソースファイルでは、XMLの要素名に対応するフィールドが作成されているのが判ると思います。今回List型のフィールド以外はpublicにしていますが、もちろんprivateにしてsetter/getterを用意する形でも問題ありません。 (今回はWebに載せるのでコード量を減らすために敢えてpublicにした)
    型はXMLのスキーマデータ型に対応する型を指定します。
    型の対応に関しては JAXBデータバインディングの使用 の標準データ型のマッピングあたりを参考にしてください。
  3. JAXBのアノテーションを付与
    XMLとの相互変換をコントロールするためのアノテーションを記述します。詳細は後述。

JAXBのアノテーション

上で利用しているJAXBのアノテーションに関して簡単に説明します。
詳細はAPIリファレンス等をあたってください。

@XmlAccessorType

Javaコード中の何がデフォルトでXMLの要素にマッピングされるかを指定します。
上ではフィールドを指定しています。
これ以外には、以下を指定可能です。

  • PROPERTY - sette/getter
  • PUBLIC_MEMBER - public なフィールド、及び、プロパティ。(これがデフォルト)
  • NONE - デフォルトではマッピングされない

@XmlRootElement

クラスをXMLの要素にマップします。
name="..." で要素名を指定できます。指定しない場合はクラス名が要素名として使われます。

@XmlElement

フィールドをXMLの要素にマップします。
name="..." で要素名を指定できます。これも未指定の場合はフィールド名が使われます。
必須(と思われる)要素に対して required="true" を指定しています。 (identifier に指定していないのは、PUT(生成)処理の際に送信するXMLにおいてはidは決まっていないため)

@XmlElementWrapper

ラッパー要素を生成します。

上のNodeクラスで properties フィールドに対して指定されています。このアノテーションがある場合、

    <properties>
        <property>
            <name>logoImg</name>
            <type>String</type>
            <multiple>false</multiple>
            <values>
                <value>jcr:a1918662-8cbe-4346-ac5a-1b24a9950e2b</value>
            </values>
        </property>
        <property>
            <name>title</name>
            <type>String</type>
            <multiple>false</multiple>
            <values>
                <value>Home</value>
            </values>
        </property>
    </properties>

ない場合

    <property>
        <name>logoImg</name>
        <type>String</type>
        <multiple>false</multiple>
        <values>
        <value>jcr:a1918662-8cbe-4346-ac5a-1b24a9950e2b</value>
        </values>
    </property>
    <property>
        <name>title</name>
        <type>String</type>
        <multiple>false</multiple>
        <values>
        <value>Home</value>
        </values>
    </property>

となります。

上記以外にもJAXBのアノテーションはありますが、比較的シンプルなXMLであれば上記のもので殆どマッピング可能なのではないかと思います。

リソース(XML)をJavaオブジェクトにマッピングする

GET処理の実装

リソースに対応するクラスが作成されたので、リソースをJavaオブジェクトにマッピングするためのコードを書いていきます。

前回 の RestClient.java に以下コードを追記します。

    public <E> E getEntity(String url, Class<E> clz, MediaType type) {
        Client client = getClient();
        WebResource resource = client.resource(url);
        ClientResponse response = resource.accept(type).get(ClientResponse.class);
        switch (response.getStatus()) {
        case 200 :  // OK
            break;
        default:
            String error = String.format("Code:%s Entity:%s",
                    response.getStatus(),
                    response.getEntity(String.class));
            throw new RuntimeException(error);
        }
        E entity = response.getEntity(clz);
        return entity;
    }

コードの中身は前回の getString メソッドと殆ど同じです。
汎用性を持たせるために、Class プロパティを渡して、このクラスにマッピングされるようにしています。
getEntity メソッドが正しく機能したら、前回の getString メソッドもこれを利用するようにリファクタリングしてしまいましょう。(但し、エラーの場合例外がスローされるようになります)

    public String getString(String url, MediaType type) {
        return getEntity(url, String.class, type);
    }

利用してみる

上記 getEntity メソッドを利用してリソースがJavaオブジェクトにマッピングされるか確認します。

RestTest.java

package jp.co.agilegroup.rest;

import javax.ws.rs.core.MediaType;

import jp.co.agilegroup.magnolia.rest.dto.Node;

public class RestTest {
    public static void main(String[] args) {
        RestClient client = new RestClient("superuser","superuser");
        String uri = "http://demo.magnolia-cms.com/.rest/nodes/v1/website/demo-project";

        Node node = client.getEntity(uri, Node.class, MediaType.APPLICATION_XML_TYPE);
        System.out.println("identifier:" + node.identifier);
        System.out.println("name:" + node.name);
        System.out.println("type:" + node.type);
        System.out.println("path:" + node.path);
    }
}

実行結果は以下のようになります。

identifier:1be12547-ad82-4c83-8396-213466ceb003
name:demo-project
type:mgnl:page
path:/demo-project

properties 等のリスト形式のフィールドへも正しく値が格納されているか確認してみてください。マッピングの指定が正しく行われていればきちんと動作します。

まとめ

【第一回】に引き続きJerseyを利用して、取得したリソースをJavaオブジェクトにマッピングするところまで実装しました。

単純な文字列で取得が行われた場合とは異なり、Javaオブジェクトにマッピング(バインディング)が行われる事で、その後の処理は劇的に簡単になります。

このように、JAX-RS/JAXB を利用する事で RESTful Webサービスからのリソースの取得、及び、その後の処理の実装を容易に行う事ができます。
特にサービスプロバイダからXMLスキーマが提供されている場合、リソースに対応するクラスは自動生成できるため、今回のRestClient.java相当のクラスがあれば、取得からオブジェクトのマッピングまでの処理を実装したとしても1時間もかからないでしょう。

Java使いでこれら機能を利用した事がないのであれば、一度使ってみてはいかがでしょうか。

 

ところで、実は今回のコードはリソースがXMLの場合限定で、JSON形式で取得しようとした場合例外がスローされて正しく取得が行えません。

JSON形式での取得に関しては 次回 に書こうと思います。