kagamihogeの日記

kagamihogeの日記です。

The Java EE 7 TutorialのJAX-RS: Advanced Topics and an Exampleの章をテキトーに訳した

The Java EE 7 Tutorial31 JAX-RS: Advanced Topics and an Exampleの章をテキトーに訳した。

31 JAX-RS: Advanced Topics and an Example

Java API for RESTful Web Services (JAX-RS, JSR 339で定義)は、開発者が容易にRESTアーキテクチャを使用できるように設計されています。この章ではJAX-RSの高度な機能について解説します。もしJAX-RSが初めてであれば、この章を読み進める前にChapter 29, "Building RESTful Web Services with JAX-RS"を参照してください。

JAX-RSは、CDIEJBServletに統合されています。

この章では以下のトピックを扱います。

  • Annotations for Field and Bean Properties of Resource Classes
  • Validating Resource Data with Bean Validation
  • Subresources and Runtime Resource Resolution
  • Integrating JAX-RS with EJB Technology and CDI
  • Conditional HTTP Requests
  • Runtime Content Negotiation
  • Using JAX-RS with JAXB
  • The customer Example Application

31.1 Annotations for Field and Bean Properties of Resource Classes

リソースクラス用のJAX-RSアノテーションによって、URIやリクエストヘッダーの指定部分から値を抽出することができます。

JAX-RSが提供するアノテーションを表 31-1に示します。

表31-1 高度なJAX-RSアノテーション

アノテーション 説明
@Context クラスフィールド、ビーンプロパティ、メソッド引数に情報をインジェクトします。
@CookieParam クッキーリクエストヘッダーで宣言されているクッキーから情報を抽出します。
@FormParam application/x-www-form-urlencodedコンテンツタイプを持つリクエスト表現から情報を抽出します。
@HeaderParam ヘッダーの値を抽出します。
@PathParam URIテンプレートパラメーターの値を抽出します。
@QueryParam URIクエリパラメータの値を抽出します。

31.1.1 Extracting Path Parameters

URIパステンプレートはURIシンタックス内に埋め込まれた変数からなるURIです。メソッドを呼ぶときに、@PathParamを使用してURIパスの変数を使うことができます。

以下のコードは従業員のe-mailアドレスが渡された時に従業員の名字を抽出する方法です。

@Path("/employees/{firstname}.{lastname}@{domain}.com")
public class EmpResource {

    @GET
    @Produces("text/xml")
    public String getEmployeelastname(@PathParam("lastname") String lastName) {
      ...
    }
}

この例では、@PathURI変数(もしくはパスパラメータ){firstname}, {lastname}, {domain}を宣言しています。リクエストメソッド引数の@PathParamがe-mailアドレスから名字を抽出します。

HTTPリクエストGET /employees/john.doe@example.comを行うと、"dow"{lastname}に入れられます。

一つのURI複数のパスパラメータを定義可能です。

URI変数に正規表現を宣言可能です。たとえば、名字が大小文字のみで構成するものだけを受け入れるようにするには、以下のような正規表現を宣言可能です。

@Path("/employees/{firstname}.{lastname[a-zA-Z]*}@{domain}.com")

名字が正規表現にマッチしない場合、404レスポンスが返されます。

31.1.2 Extracting Query Parameters

@QueryParamを使用してリクエストURIのクエリコンポーネントからクエリパラメータを抽出できます。

たとえば、指定範囲内に入社した全従業員を問い合わせるために、以下のようなメソッドシグネチャを使用できます。

@Path("/employees/")
@GET
public Response getEmployees(
        @DefaultValue("2003") @QueryParam("minyear") int minyear,
        @DefaultValue("2013") @QueryParam("maxyear") int maxyear)
    {...}

このコードは二つのクエリパラメータ、minyearmaxyearを定義しています。以下のHTTPリエクストで2003年から2013年に入社した全従業員を問い合わせます。

GET /employees?maxyear=2013&minyear=2003

@DefaultValueはデフォルト値を定義しており、もしクエリパラメータとなる値が無ければこの値が使われます。デフォルトでは、JAX-RSは、objectにはnull、プリミティブデータ型にはゼロ、を割り当てます。nullやゼロを避けるために@DefaultValueで任意のデフォルト値を設定可能です。

31.1.3 Extracting Form Data

HTMLフォームからパラメータを抽出するには@FormParamを使用します。たとえば、以下のフォームには、氏名・住所・従業員のマネージャ名、を入力します。

<FORM action="http://example.com/employees/" method="post">
  <p>
    <fieldset>
      Employee name: <INPUT type="text" name="empname" tabindex="1">  
      Employee address: <INPUT type="text" name="empaddress" tabindex="2"> 
      Manager name: <INPUT type="text" name="managername" tabindex="3"> 
    </fieldset>
  </p>
</FORM>

HTMLフォームからマネージャ名を抽出するには以下のコードを使用します。

@POST
@Consumes("application/x-www-form-urlencoded")
public void post(@FormParam("managername") String managername) {
    // Store the value
    ...
}

フォームパラメータの名前と値のマップを取得するには以下のようなコードを使用します。

@POST
@Consumes("application/x-www-form-urlencoded")
public void post(MultivaluedMap<String, String> formParams) {
    // Store the message
}

31.1.4 Extracting the Java Type of a Request or Response

javax.ws.rs.core.Contextはリクエストやレスポンスに関するJavaの型を取得します。

javax.ws.rs.core.UriInfoインタフェースはリクエストURIコンポーネントに関する情報を提供します。以下のコードはクエリとパスパラメータのkey/valueマップを取得します。

@GET
public String getParams(@Context UriInfo ui) {
    MultivaluedMap<String, String> queryParams = ui.getQueryParameters();
    MultivaluedMap<String, String> pathParams = ui.getPathParameters();
}

javax.ws.rs.core.HttpHeadersインタフェースはリクエストヘッダーとクッキーに関する情報を取得します。以下のコードはヘッダーとクッキーパラメータのkey/valueマップを取得します。

@GET
public String getHeaders(@Context HttpHeaders hh) {
    MultivaluedMap<String, String> headerParams = hh.getRequestHeaders();
    MultivaluedMap<String, Cookie> pathParams = hh.getCookies();
}

31.2 Validating Resource Data with Bean Validation

JAX-RSは、リソースクラスを検証するためのBean Validationをサポートしています。

  • リソースメソッドパラメータに制約アノテーションの追加。
  • エンティティがパラメータとして渡された時にエンティティデータが妥当なことを保証。

31.2.1 Using Constraint Annotations on Resource Methods

Bean Validation制約アノテーションはリソースのパラメータに適用可能です。サーバはパラメータを検証し、通過させるかjavax.validation.ValidationException例外をスローします。

@POST
@Path("/createUser")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public void createUser(@NotNull @FormParam("username") String username,
                       @NotNull @FormParam("firstName") String firstName,
                       @NotNull @FormParam("lastName") String lastName,
                       @Email @FormParam("email") String email) {
    ...
}

この例では、ビルトインの制約@NotNullをフォームフィールドのusername, firstName, lastNameに適用しています。ユーザ定義の@Email制約はemailフォームフィールドのemailアドレスが正しいフォーマットかどうかを検証します。

また、制約はリソースクラス内のフィールドにも適用可能です。

@Path("/createUser")
public class CreateUserResource {
  @NotNull
  @FormParam("username")
  private String username;

  @NotNull
  @FormParam("firstName")
  private String firstName;

  @NotNull
  @FormParam("lastName")
  private String lastName;

  @Email
  @FormParam("email")
  private String email;

  ...
}

この例では、一つ前のサンプルではメソッド引数に適用されていた制約と、同様のものがクラスフィールドに適用されています。両者の振る舞いは同じものになります。

また、getterメソッドに制約アノテーションを付与することで、制約をリソースクラスのJavaBeansプロパティにも適用可能です。

@Path("/createuser")
public class CreateUserResource {
  private String username;

  @FormParam("username")
  public void setUsername(String username) {
    this.username = username;
  }

  @NotNull
  public String getUsername() {
    return username;
  }
  ...
}

また、制約はリソースのクラスレベルにも適用可能です。以下の例では、ユーザ定義制約@PhoneRequiredが、ユーザが少なくとも一つの電話番号を入力していることを保証します。homePhoneもしくはmobilePhoneのどちらかがnullになることはあっても、両方ともnullにはなりません。

@Path("/createUser")
@PhoneRequired
public class CreateUserResource {
  @FormParam("homePhone")
  private Phone homePhone;

  @FormParam("mobilePhone")
  private Phone mobilePhone;
  ...
}

31.2.2 Validating Entity Data

validation制約アノテーションを含むクラスはリソースクラスのメソッド引数で使用可能です。エンティティクラスを検証するには、メソッド引数に@Validを付与します。たとえば、以下のクラスは標準とユーザ定義validation制約の両方を含むユーザ定義クラスです。

@PhoneRequired
public class User {
  @NotNull
  private String username;

  private Phone homePhone;

  private Phone mobilePhone;
  ...
}

エンティティクラスはリソースメソッドの引数として使用可能です。

@Path("/createUser")
public class CreateUserResource {
  ...
  @POST
  @Consumers(MediaType.APPLICATION_XML)
  public void createUser(@Valid User user) {
    ...
  }
  ...
}

@Validはエンティテクラスが実行時に妥当であることを保証します。また、ユーザ定義制約をエンティテイvalidationに追加することも出来ます。

@Path("/createUser")
public class CreateUserResource {
  ...
  @POST
  @Consumers(MediaType.APPLICATION_XML)
  public void createUser(@ActiveUser User user) {
    ...
  }
  ...
}

この例では、ユーザ定義の@ActiveUser制約が、エンティティクラス内に定義された@PhoneRequired@NotNullに加えて、Userクラスに適用されます。

リソースメソッドがエンティティクラスを返す場合、validationは@Validを適用するかリソースメソッドにユーザ定義の制約アノテーションを付与することで実行されます。

@Path("/getUser")
public class GetUserResource {
  ...
  @GET
  @Path("{username}")
  @Produces(MediaType.APPLICATION_XML)
  @ActiveUser
  @Valid
  public User getUser(@PathParam("username") String username) {
    // find the User
    return user;
  }
  ...
}

この例では、@ActiveUser制約が、エンティティクラス内に定義された@PhoneRequired@NotNull制約と同様に、戻り値のクラスに適用されます。

31.2.3 Validation Exception Handling and Response Codes

javax.validation.ValidationExceptionConstraintValidationExceptionを除くValidationExceptionのサブクラスがスローされる場合、JAX-RSランタイムはクライアントリクエストに対しHTTPステータスコード500 (Internal Server Error)を返します。

ConstraintValidationExceptionがスローされる場合、JAX-RSランタイムは以下のHTTPステータスコードの内いずれか一つをクライアントに返します。

31.3 Subresources and Runtime Resource Resolution

URIリクエストの一部としてのみ処理するリソースクラスを使用し、URIパスの残りを処理するサブリソースをルートリソースで実装することが可能です。

@Pathを付与したリソースクラスのメソッドは、サブリソースメソッドかサブリソースロケーターのどちらかになれます。

  • サブリソースメソッドは、対応するリソースのサブリソースのリクエストを処理するために使用します。
  • サブリソースロケーターは、対応するリソースのサブリソースの場所を示すために使用します。

31.3.1 Subresource Methods

サブリソースメソッド(subresource method)はHTTPリクエストを直接処理します。メソッドは、@GET@POSTなどのrequest method designatorと、@Pathを付与しなければなりません。リソースクラスとメソッドURIテンプレートを繋げ、それによって作成されたURIテンプレートとリクエストURIがマッチする場合、メソッドが呼ばれます。

以下のコードは、従業員のemailアドレスが渡されたとき、従業員の名字を抽出するためにサブリソースメソッドを使用している例です。

@Path("/employeeinfo")
public class EmployeeInfo {

    public employeeinfo() {}

    @GET
    @Path("/employees/{firstname}.{lastname}@{domain}.com")
    @Produces("text/xml")
    public String getEmployeeLastName(@PathParam("lastname") String lastName) {
       ...
    }
}

getEmployeeLastNameメソッドは以下のGETリクエストでdoeを返します。

GET /employeeinfo/employees/john.doe@example.com

31.3.2 Subresource Locators

サブリソースロケーター(subresource locator)は、HTTPリクエストを処理するオブジェクトを返します。メソッドにはrequest method designatorを付与する必要はありません。サブリソース内でサブリソースロケーターを宣言する必要があり、サブリソースロケーターのみ実行時のリソース解決に使用されます。

以下のコードがサブリソースロケーターの例です。

// Root resource class
@Path("/employeeinfo")
public class EmployeeInfo {

    //     Subresource locator: obtains the subresource Employee
    // from the path /employeeinfo/employees/{empid}
    @Path("/employees/{empid}")
    public Employee getEmployee(@PathParam("empid") String id) {
        // Find the Employee based on the id path parameter
        Employee emp = ...;
        ...
        return emp;
    }
}
// Subresource class
public class Employee {

    // Subresource method: returns the employee's last name
    @GET
    @Path("/lastname")
    public String getEmployeeLastName() {
        ...
        return lastName;
    }
}

このコードでは、getEmployeeメソッドEmployeeを提供するサブリソースロケーターで、lastname用のサービスリクエストを持ちます。

HTTPリクエストがGET /employeeinfo/employees/as209/の場合、getEmployeeas209を持つEmployeeを返します。実行時に、JAX-RSGET /employeeinfo/employees/as209/lastnameリクエストをgetEmployeeLastNameに送ります。getEmployeeLastNameはemployeeの名字にas209を持つ値を返します。

31.4 Integrating JAX-RS with EJB Technology and CDI

JAX-RSEJBCDIと共に動作します。

一般的に、JAX-RSEJBを組み合わせるには、beanクラスに@Pathを付与してルートリソースクラスにする必要があります。@Pathをstateless session beanやシングルトンビーンに付与可能です。

以下のコードはstateless session beanやシングルトンビーンをJAX-RSのルートリソースクラスにしています。

@Stateless
@Path("stateless-bean")
public class StatelessResource {...}
@Singleton
@Path("singleton-bean")
public class SingletonResource {...}

また、session beanはサブリソースにもできます。

JAX-RSCDIコンポーネントモデルが若干異なります。デフォルトでは、JAX-RSのルートリソースクラスはリクエストスコープで管理されており、スコープを指定するアノテーションは必要とされません。@RequestScoped@ApplicationScopedを付与したCDIのマネージドビーンはJAX-RSのリソースクラスにできます。

以下のコードはJAX-RSのリソースクラスの例です。

@Path("/employee/{id}")
public class Employee {
    public Employee(@PathParam("id") String id) {...}
}
@Path("{lastname}")
public final class EmpDetails {...}

以下のコードはJAX-RSリソースクラスをCDIビーンにした例です。ビーンはproxyable*1でなければならないので、Employeeは非privateで引数無しのコンストラクタを持つ必要があり、EmpDetailsは非finalでなければなりません。

@Path("/employee/{id}")
@RequestScoped
public class Employee {
    public Employee() {...}

    @Inject
    public Employee(@PathParam("id") String id) {...}
}
@Path("{lastname}")
@RequestScoped
public class EmpDetails {...}

31.5 Conditional HTTP Requests

JAX-RSは条件付き(conditional) GET, PUT HTTPリクエストをサポートしています。条件付きGETリクエストはクライアント処理の効率を改善することで帯域を節約します。

GETリクエストは、表現が以前のリクエストから変更されていない場合には、Not Modified (304)レスポンスを返すことが可能です。たとえば、webサイトは、以前のリクエストから変更されていないすべての静的イメージに対して、304レスポンスを返すことが可能です。

PUTリクエストは、表現が最後のリクエストから変更されていない場合には、Precondition Failed (412)レスポンスを返すことが可能です。条件付きPUTによって、ロストアップデート問題(lost update problem)*2を避けることができます。

条件付きHTTPリクエストではLast-ModifiedETagヘッダーを使用可能です。Last-Modifiedヘッダーは秒単位の粒度で日時を表現可能です。

@Path("/employee/{joiningdate}")
public class Employee {

    Date joiningdate;
    
    @GET
    @Produces("application/xml")    
    public Employee(@PathParam("joiningdate") Date joiningdate, 
                    @Context Request req, 
                    @Context UriInfo ui) {

        this.joiningdate = joiningdate;
        ...
        this.tag = computeEntityTag(ui.getRequestUri());
        if (req.getMethod().equals("GET")) {
            Response.ResponseBuilder rb = req.evaluatePreconditions(tag);
            if (rb != null) {
                throw new WebApplicationException(rb.build());
            }
        }
    }
}

このコードでは、EmployeeコンストラクタはリクエストURIからentity tagを算出し、そのタグを引数にrequest.evaluatePreconditionsメソッドを呼び出します。もしクライアントリクエストが、算出したentity tagと同じ値を持つIf-none-matchヘッダーを返す場合、evaluate.Preconditionsは304ステータスコードとentity tagが設定されたpre-filled-outレスポンスを返します。

31.6 Runtime Content Negotiation

@Produces@ConsumesJAX-RSで静的なコンテンツネゴシエーションを処理します。このアノテーションにはサーバーのコンテンツ設定を指定します。Accept, Content-Type, Accept-LanguageなどのHTTPヘッダーでクライアントのコンテンツネゴシエーション設定を定義します。

コンテンツネゴシエーション用のHTTPヘッダーに関する詳細はHTTP /1.1 - Content Negotiationを参照してください(http://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html)。

以下のコードはサーバーのコンテンツ設定の例です。

@Produces("text/plain")
@Path("/employee")
public class Employee {

    @GET
    public String getEmployeeAddressText(String address) {...}

    @Produces("text/xml")
    @GET
    public String getEmployeeAddressXml(Address address) {...}
}

以下のようなHTTPリクエストでgetEmployeeAddressTextが呼び出されます。

GET /employee
Accept: text/plain

これは以下のようなレスポンスを生成します。

500 Oracle Parkway, Redwood Shores, CA

以下のようなHTTPリクエストでgetEmployeeAddressXmlが呼び出されます。

GET /employee
Accept: text/xml

これは以下のようなレスポンスを生成します。

<address street="500 Oracle Parkway, Redwood Shores, CA" country="USA"/>

静的なコンテンツネゴシエーションでは、クライアントとサーバに複数のコンテンツとメディアタイプを定義可能です。

@Produces("text/plain", "text/xml")

静的なコンテンツネゴシエーションに加えて、JAX-RSjavax.ws.rs.core.VariantRequestを使用したランタイムコンテンツネゴシエーションをサポートしています。Variantはコンテンツネゴシエーションのリソース表現を指定します。Variantの各インスタンスは、メディアタイプ・言語・エンコーディングを持つことが出来ます。Variantオブジェクトはサーバがサポートするリソース表現を定義します。表現のvariantのリストを構築するにはVariant.VariantListBuilderを使用します。

以下のコードはリソース表現variantリストを生成する方法を示しています。

List<Variant> vs = Variant.mediatypes("application/xml", "application/json")
        .languages("en", "fr").build();

このコードはVariantListBuilderbuildメソッドを呼んでいます。mediatypes, languages, encodingsメソッドを呼ぶ場合にVariantListBuilderが呼び出されます。リソース表現の組み合わせをビルドするにはbuildメソッドを使用します。buildメソッドで作成したVariantのリストが、mediatypes, languages, encodingsで指定した項目の組み合わせとなります。

この例では、vsオブジェクトのサイズは4になり、その内訳は以下となります。

[["application/xml","en"], ["application/json","en"],
    ["application/xml","fr"],["application/json","fr"]]

javax.ws.rs.core.Request.selectVariantメソッドVariantオブジェクトのリストを受け入れ、HTTPリクエストにマッチするVariantオブジェクトを選択します。このメソッドVariantオブジェクトのリストと、HTTPリクエストヘッダーのAccept, Accept-Encoding, Accept-Language, Accept-Charsetとを比較します。

以下のコードはselectVariantメソッドを使用して、クライアントリクエストの値から受入可能なVariantを選択する方法を示しています。

@GET
public Response get(@Context Request r) { 
    List<Variant> vs = ...;
    Variant v = r.selectVariant(vs);
    if (v == null) {
        return Response.notAcceptable(vs).build();
    } else {
        Object rep = selectRepresentation(v);
        return Response.ok(rep, v);
    }
}

selectVariantはリクエストにマッチするVariantオブジェクトを返し、もしマッチするものがなければnullになります。このコードでは、メソッドがnullを返す場合は、受入不能レスポンス(nonacceptable response)用のResponseオブジェクトを構築します。そうでない場合は、VariantObjectエンティティ形式の表現をOKステータスで保持するResponseを返します。

31.7 Using JAX-RS with JAXB

Java Architecture for XML Binding (JAXB)はXML-to-Javaバインディング技術で、スキーマJavaオブジェクトの変換と、XMLインスタンスドキュメントとJavaオブジェクトインスタンス間の変換を可能にすることで、webサービスの開発を簡易化します。XMLスキーマはデータ要素とXMLドキュメントの構造を定義します。JavaクラスとXMLスキーマ間のマッピングを確立するのにツールとJAXB APIを使用可能です。JAXBはXMLドキュメントとJavaオブジェクトの相互変換を可能にするツールを提供します。

JAXBを使用することで、以下のような方法でデータオブジェクトが操作可能になります。

XMLはRESTfulサービスが生成および受入するメディアフォーマットとして一般的に使用されています。XMLをデシリアライズ/シリアライズには、JAXBアノテーションが付与されたオブジェクトでリクエストやレスポンスを表現可能です。JAX-RSアプリケーションではXMLデータの操作にJAXBオブジェクトを使用可能です。JAXBオブジェクトはリクエストエンティティパラメータとレスポンスエンティティで使用可能です。JAXBオブジェクトをエンティティとして読み込みと書き込みを行うための標準プロバイダインタフェースMessageBodyReaderMessageBodyWriterが、JAX-RSランタイム環境には含まれています。

JAX-RSでは、リソースをパブリッシュすることでサービスがアクセス可能になります。リソースはただ単にJAX-RSアノテーションをいくつか付与しただけのJavaクラスです。そのアノテーションは以下を表現しています。

アプリケーションにおけるリソースを定義するには、公開したいデータのタイプを考える必要があります。個々のユーザ環境には、ユーザに公開したい情報が格納されている関係データベースが既にあるかもしれませんし、データベース内には存在しないがリソースとしては配布する必要のある静的コンテンツを持っているかもしれません。JAX-RSでは、複数ソースからのコンテンツ配布が可能です。RESTful webサービスはリクエストとレスポンスに様々なタイプの入力と出力フォーマットを使用可能です。The customer Example Applicationで解説されているcustomerサンプルはXMLを使用しています。

リソースは表現を持ちます。リソースの表現とはHTTPメッセージのコンテンツであり、URIを使用したリソースに送信または受信されます。それぞれのリソース表現はその表現に対応するメディアタイプを持ちます。たとえば、もしリソースがXMLフォーマットのコンテンツを返す場合、HTTPメッセージの関連メディアタイプとしてapplication/xmlを使用可能です。アプリケーションの要求に応じて、単一のフォーマットか複数のフォーマットか、リソースは望みの条件で表現を返すことが可能です。読み込みと書き込みを行うリソースメソッドが受け入れ可能なメディアタイプを宣言するのに、JAX-RSでは@Consumes@Producesを提供しています。

また、JAX-RSはエンティティプロバイダを使用してJavaの型とリソース表現との相互マッピングを行います。MessageBodyReaderエンティテイプロバイダはリクエストエンティテイを読み込んでJavaの型にデシリアライズします。MessageBodyWriterエンティテイプロバイダはJavaの型からレスポンスエンティティにシリアライズします。たとえば、Stringをリクエストエンティティパラメータとして使用する場合、MessageBodyReaderエンティテイプロバイダはリクエストボディにStringをデシリアライズします。もしJAXBの型をリソースメソッドの戻り値型として使用する場合、MessageBodyWriterはJAXBオブジェクトをレスポンスボディにシリアライズします。

デフォルトでは、JAX-RSランタイム環境はJAXBクラス用のデフォルトのJAXBContextクラスを生成して使用を試行します。しかし、もしデフォルトのJAXBContextクラスが適さない場合、JAX-RSContextResolverプロバイダーインタフェースを使用してアプリケーションにJAXBContextクラスを提供出来ます。

以降のセクションではJAX-RSリソースメソッドでのJAXBの使用方法を解説します。

31.7.1 Using Java Objects to Model Your Data

もし公開したいデータ用のXMLスキーマ定義が無い場合、データをJavaクラスとしてモデル化し、JAXBアノテーションを付与して、XMLスキーマを生成できます。たとえば、公開したいデータが製品とそのID・氏名・説明・価格のコレクションな場合、以下のようなJavaクラスとしてモデル化が可能です。

@XmlRootElement(name="product")
@XmlAccessorType(XmlAccessType.FIELD)
public class Product {

    @XmlElement(required=true)
    protected int id;
    @XmlElement(required=true)
    protected String name;
    @XmlElement(required=true)
    protected String description;
    @XmlElement(required=true)
    protected int price;

    public Product() {}

    // Getter and setter methods
    // ...
}

対応するXMLスキーマ定義を生成するためにコマンドラインでJAXBスキーマジェネレーターを実行します。

schemagen Product.java

このコマンドは.xsdファイルとしてXMLスキーマを生成します。

<?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="product" type="product"/>

    <xs:complexType name="product">
      <xs:sequence>
        <xs:element name="id" type="xs:int"/>
        <xs:element name="name" type="xs:string"/>
        <xs:element name="description" type="xs:string"/>
        <xs:element name="price" type="xs:int"/>
      </xs:sequence>
    <xs:complexType>
</xs:schema>

マッピング完了後は、アプリケーションでProductを生成し、戻り値にしたり、JAX-RSリソースメソッドの引数として使用が可能になります。JAX-RSランタイムはJAXBを使用して、リクエストのXMLデータをProductオブジェクトに変換したり、Productオブジェクトをレスポンス用のXMLデータに変換したりします。以下のリソースクラスが簡単な例になります。

@Path("/product")
public class ProductService {
    @GET
    @Path("/get")
    @Produces("application/xml")
    public Product getProduct() {
        Product prod = new Product();
        prod.setId(1);
        prod.setName("Mattress");
        prod.setDescription("Queen size mattress");
        prod.setPrice(500);
        return prod;
    }

    @POST
    @Path("/create")
    @Consumes("application/xml")
    public Response createProduct(Product prod) {
        // Process or store the product and return a response
        // ...
    }
}

NetBeans IDEなどの一部のIDEでは、プロジェクトにJAXBアノテーション付与したクラスを追加すると、ビルド処理中に自動的にスキーマジェネレータツールを実行するものもあります。詳細な例については、The customer Example Applicationを参照してください。customerサンプルにはデータをモデル化するJavaクラス間に複雑な関連を持つものが含まれており、これは深い階層を持つXML表現を生成します。

31.7.2 Starting from an Existing XML Schema Definition

公開したいデータ用に.xsdファイルでXMLスキーマ定義を作成済みの場合、JAXBスキーマコンパイラツールを使用できます。.xsdファイルの簡単な例を考えてみます。

<xs:schema targetNamespace="http://xml.product"
           xmlns:xs="http://www.w3.org/2001/XMLSchema"
           elementFormDefault="qualified"
           xmlns:myco="http://xml.product">
  <xs:element name="product" type="myco:Product"/>
  <xs:complexType name="Product">
    <xs:sequence>
      <xs:element name="id" type="xs:int"/>
      <xs:element name="name" type="xs:string"/>
      <xs:element name="description" type="xs:string"/>
      <xs:element name="price" type="xs:int"/>
    </xs:sequence>
  </xs:complexType>
</xs:schema>

コマンドラインで以下のようにしてスキーマコンパイラツールを実行します。

xjc Product.xsd

このコマンドは.xsdファイルで定義されている型に対応するJavaクラスのソースコードを生成します。スキーマコンパイラツール.xsdファイルで定義されているcomplexTypeごとにJavaクラスを生成します。生成されるJavaクラスの各フィールドはcomplexType内の要素に相当し、クラスにはそのフィールド用のgetter/setterメソッドが含まれます。

上記サンプルの場合、スキーマコンパイラツールproduct.xml.Productproduct.xml.ObjectFactoryクラスを生成します。ProductクラスにはJAXBアノテーション.xsd定義に対応するフィールドが含まれます。

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Product", propOrder = {
    "id",
    "name",
    "description",
    "price"
})
public class Product {
    protected int id;
    @XmlElement(required = true)
    protected String name;
    @XmlElement(required = true)
    protected String description;
    protected int price;

    // Setter and getter methods
    // ...
}

アプリケーションでProductクラスのインスタンスを(たとえばDBの値を用いて)生成できます。オブジェクトをJAXB要素に変換するためのメソッドが生成されたproduct.xml.ObjectFactoryに含まれます。このメソッドJAX-RSリソースメソッド内でXMLを返すために使用可能です。

@XmlElementDecl(namespace = "http://xml.product", name = "product")
public JAXBElement<Product> createProduct(Product value) {
    return new JAXBElement<Product>(_Product_QNAME, Product.class, null, value);
}

以下のコードはJAX-RSメソッドXMLとしてJAXB要素を返すのに生成クラスを使用する例を示しています。

@Path("/product")
public class ProductService {
    @GET
    @Path("/get")
    @Produces("application/xml")
    public JAXBElement<Product> getProduct() {
        Product prod = new Product();
        prod.setId(1);
        prod.setName("Mattress");
        prod.setDescription("Queen size mattress");
        prod.setPrice(500);
        return new ObjectFactory().createProduct(prod);
    }
}

@POST@PUTのリソースメソッドでは、引数として直接Productを使用可能です。JAX-RSはリクエストのXMLデータをProductオブジェクトにマッピングします。

@Path("/product")
public class ProductService {
    @GET
    // ...

    @POST
    @Path("/create")
    @Consumes("application/xml")
    public Response createProduct(Product prod) {
        // Process or store the product and return a response
        // ...
    }
}

31.7.3 Using JSON with JAX-RS and JAXB

JAX-RSはJAXBを使用してXMLの読み込みと書き込みを自動的に出来ますが、JSONデータも同様に処理出来ます。JSONJavaScriptが生成するシンプルなテキストベースのデータ交換フォーマットです。前述の例で言うと、製品のXML表現は以下のようになりますが、

<?xml version="1.0" encoding="UTF-8"?>
<product>
  <id>1</id>
  <name>Mattress</name>
  <description>Queen size mattress</description>
  <price>500</price>
</product>

同等なJSON表現は以下のようになります。

{
    "id":"1",
    "name":"Mattress",
    "description":"Queen size mattress",
    "price":500
}

JSONデータのレスポンスを生成するためにリソースメソッドでは@Producesapplication/jsonMediaType.APPLICATION_JSONフォーマットを追加可能です。

@GET
@Path("/get")
@Produces({"application/xml","application/json"})
public Product getProduct() { ... }

この例では、デフォルトのレスポンスはXMLですが、クライアントがGETリクエストのヘッダーに以下を含める場合にはJSONオブジェクトがレスポンスとなります。

Accept: application/json

リソースメソッドはJAXBアノテーションの付与クラスされたクラスでJSONデータを受けることも可能です。

@POST
@Path("/create")
@Consumes({"application/xml","application/json"})
public Response createProduct(Product prod) { ... }

POSTリクエストでJSONデータをサブミットする場合、クライアントには以下のヘッダーを含めます。

Content-Type: application/json

31.8 The customer Example Application

このセクションではcustomerサンプルアプリケーションのビルドと実行方法について解説します。このアプリケーションはRESTful webサービスで、JAXBを使用して特定エンティティの生成・読み込み・更新・削除(CRUD)を行います。

customerサンプルアプリケーションはtut-install/examples/jaxrs/customer/*3ディレクトリにあります。サンプルアプリケーションの基本的なビルドと実行についてはChapter 2, "Using the Tutorial Examples,"を参照してください。

31.8.1 Overview of the customer Example Application

アプリケーションのソースファイルはtut-install/examples/jaxrs/customer/src/main/java/にあります。また、アプリケーションは三つの部分に分けられます。

  • CustomerAddressのエンティティクラス。このクラスはアプリケーションのモデルデータでJAXBアノテーションを含みます。
  • CustomerServiceリソースクラス。このクラスにはJAX-RSのリソースメソッドが含まれます。このメソッドCustomerインスタンス上の操作を実行し、このインスタンスはJAXBを使用してJSONデータやXMLとして表現されるものです。
  • CustomerBeanセッションビーンwebクライアント用のバッキングビーンとして振る舞います。CustomerBeanCustomerServiceメソッドを呼ぶのにJAX-RSクライアントAPIを使用します。

customerサンプルアプリケーションはJAXBアノテーションを用いてJavaクラスとしてデータエンティティをモデル化する方法を示しています。

31.8.2 The Customer and Address Entity Classes

以下のクラスは顧客の住所を表現しています。

@Entity
@Table(name="CUSTOMER_ADDRESS")
@XmlRootElement(name="address")
@XmlAccessorType(XmlAccessType.FIELD)
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    
    @XmlElement(required=true) 
    protected int number;
    
    @XmlElement(required=true)  
    protected String street;
    
    @XmlElement(required=true)  
    protected String city;
    
    @XmlElement(required=true) 
    protected String province;
    
    @XmlElement(required=true)  
    protected String zip;
    
    @XmlElement(required=true)
    protected String country;
    
    public Address() { }
    
    // Getter and setter methods
    // ...
}

@XmlRootElement(name="address")はこのクラスをaddressXML要素にマッピングします。@XmlAccessorType(XmlAccessType.FIELD)はこのクラスのすべてフィールドがデフォルトでXMLにバインドされることを意味します。@XmlElement(required=true)はこの要素がXML表現に存在しなければならないことを意味します。

以下のクラスは顧客を表現するものです。

@Entity
@Table(name="CUSTOMER_CUSTOMER")
@NamedQuery(
    name="findAllCustomers",
    query="SELECT c FROM Customer c " +
          "ORDER BY c.id"
)
@XmlRootElement(name="customer")
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @XmlAttribute(required=true) 
    protected int id;
    
    @XmlElement(required=true) 
    protected String firstname;
    
    @XmlElement(required=true) 
    protected String lastname;
    
    @XmlElement(required=true)
    @OneToOne
    protected Address address;
    
    @XmlElement(required=true)
    protected String email;
    
    @XmlElement (required=true)
    protected String phone;
    
    public Customer() {...}
    
    // Getter and setter methods
    // ...
}

Customerクラスは@XmlAttribute(required=true)を除いて前述のクラスと同様なJAXBアノテーションを持っています。@XmlAttribute(required=true)XML要素表現クラスの属性とプロパティをマッピングします。

Customerクラスは別のエンティテイであるAddressクラス型のプロパティを持ちます。この仕組みはエンティティ間の階層関係を.xsdを書かずにJavaコードで定義可能なものです。

JAXBは前述の二つのクラスで以下のXMLスキーマ定義を生成します。

<?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="address" type="address"/>
  <xs:element name="customer" type="customer"/>

  <xs:complexType name="address">
    <xs:sequence>
      <xs:element name="id" type="xs:long" minOccurs="0"/>
      <xs:element name="number" type="xs:int"/>
      <xs:element name="street" type="xs:string"/>
      <xs:element name="city" type="xs:string"/>
      <xs:element name="province" type="xs:string"/>
      <xs:element name="zip" type="xs:string"/>
      <xs:element name="country" type="xs:string"/>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="customer">
    <xs:sequence>
      <xs:element name="firstname" type="xs:string"/>
      <xs:element name="lastname" type="xs:string"/>
      <xs:element ref="address"/>
      <xs:element name="email" type="xs:string"/>
      <xs:element name="phone" type="xs:string"/>
    </xs:sequence>
    <xs:attribute name="id" type="xs:int" use="required"/>
  </xs:complexType>
</xs:schema>

31.8.3 The CustomerService Class

CustomerServiceクラスのcreateCustomerメソッドCustomerベースの顧客リソースを生成して新しいリソースのURIを返します。

@Stateless
@Path("/Customer")
public class CustomerService {
    public static final Logger logger =
            Logger.getLogger(CustomerService.class.getCanonicalName());
    @PersistenceContext
    private EntityManager em;
    private CriteriaBuilder cb;

    @PostConstruct
    private void init() {
        cb = em.getCriteriaBuilder();
    }
    ...
    @POST
    @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public Response createCustomer(Customer customer) {
 
        try {
            long customerId = persist(customer);
            return Response.created(URI.create("/" + customerId)).build();
        } catch (Exception e) {
            logger.log(Level.SEVERE,
                    "Error creating customer for customerId {0}. {1}",
                    new Object[]{customer.getId(), e.getMessage()});
            throw new WebApplicationException(e,
                    Response.Status.INTERNAL_SERVER_ERROR);
        }
    }
    ...
    private long persist(Customer customer) {
        try {
            Address address = customer.getAddress();
            em.persist(address);
            em.persist(customer);
        } catch (Exception ex) {
            logger.warning("Something went wrong when persisting the customer");
        }
        return customer.getId();
    }

クライアントに返すレスポンスには新規作成されたリソースのURIが含まれています。戻り値の型はレスポンスのプロパティをマッピングしたエンティティボディです、また、レスポンスのステータスプロパティでステータスコードを指定します。WebApplicationExceptionは404, 406, 415, 500などの適切なHTTPエラーステータスコードをラップするためのRuntimeExceptionです。

@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})および@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})は適切なMIMEクライアントを使用するためにリクエストレスポンスのメディタタイプを設定しています。このアノテーションはリソースメソッド・リソースクラスあるいはエンティテイプロバイダにに適用可能です。もしこれらのアノテーションを使用しない場合、JAX-RSは任意のメディアタイプ("*/*")の使用を許可します。

以下のコードはgetCustomerfindbyIdメソッドの実装を示しています。getCustomerメソッド@Producesを使用してCustomerオブジェクトを返し、このオブジェクトはクライアントが指定するAccept:に応じてXMLもしくはJSON表現に変換されます。

    @GET
    @Path("{id}")
    @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public Customer getCustomer(@PathParam("id") String customerId) {
        Customer customer = null;
 
        try {
            customer = findById(customerId);
        } catch (Exception ex) {
            logger.log(Level.SEVERE,
                    "Error calling findCustomer() for customerId {0}. {1}",
                    new Object[]{customerId, ex.getMessage()});
        }
        return customer;
    }
    ...
    private Customer findById(String customerId) {
        Customer customer = null;
        try {
            customer = em.find(Customer.class, customerId);
            return customer;
        } catch (Exception ex) {
            logger.log(Level.WARNING,
                    "Couldn't find customer with ID of {0}", customerId);
        }
        return customer;
    }

31.8.4 Using the JAX-RS Client in the CustomerBean Classes

customerサンプルアプリケーションではクライアントの記述にはJAX-RS Client APIを使用しています。

CustomerBeanEJBクラスがCustomerServicewebサービスのテストにJAX-RS Client APIを呼び出しています。

@Named
@Stateless
public class CustomerBean {
    protected Client client;
    private static final Logger logger = 
            Logger.getLogger(CustomerBean.class.getName());
    
    @PostConstruct
    private void init() {
        client = ClientBuilder.newClient();
    }
 
    @PreDestroy
    private void clean() {
        client.close();
    }

    public String createCustomer(Customer customer) {
        if (customer == null) {
            logger.log(Level.WARNING, "customer is null.");
            return "customerError";
        }
        String navigation;
        Response response =
                client.target("http://localhost:8080/customer/webapi/Customer")
                .request(MediaType.APPLICATION_XML)
                .post(Entity.entity(customer, MediaType.APPLICATION_XML),
                        Response.class);
        if (response.getStatus() == Status.CREATED.getStatusCode()) {
            navigation = "customerCreated";
        } else {
            logger.log(Level.WARNING, "couldn''t create customer with " +
                    "id {0}. Status returned was {1}", 
                    new Object[]{customer.getId(), response.getStatus()});
            navigation = "customerError";
        }
        return navigation;
    }
 
    public String retrieveCustomer(String id) {
        String navigation;
        Customer customer =
                client.target("http://localhost:8080/customer/webapi/Customer")
                .path(id)
                .request(MediaType.APPLICATION_XML)
                .get(Customer.class);
        if (customer == null) {
            navigation = "customerError";
        } else {
            navigation = "customerRetrieved";
        }
        return navigation;
    }
    
    public List<Customer> retrieveAllCustomers() {
        List<Customer> customers =
                client.target("http://localhost:8080/customer/webapi/Customer")
                .path("all")
                .request(MediaType.APPLICATION_XML)
                .get(new GenericType<List<Customer>>() {});
        return customers;
    }
}

このクライアントはPOSTGETメソッドを使用しています。

正常終了を示すHTTPステータスコードは、POSTでは201、GETでは200、DELETEでは204です。HTTPステータスコードの詳細な意味については http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html を参照してください。

31.8.5 Running the customer Example

このセクションではNetBeans IDEMavenのどちらかを使用して、customerサンプルのビルド・パッケージング・デプロイ・実行を行う手順を説明します。

31.8.5.1 To Build, Package, and Deploy the customer Example Using NetBeans IDE

  1. GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
  2. FileメニューからOpen Projectを選択
  3. Open Projectダイアログボックスから以下のディレクトリに移動。
    tut-install/examples/jaxrs*4
  4. customerフォルダを選択。
  5. Open Projectをクリック。
  6. Projectsタブでcustomerプロジェクトを右クリックしてBuildを選択。
    このコマンドはtargetディレクトリにアプリケーションをcustomer.warファイルにビルドしてパッケージングし、それからWARファイルをGlassFish Serverにデプロイします。
  7. 以下のURLでブラウザのwebクライアントを開きます。
    http://localhost:8080/customer/
    webクライアントで顧客の作成と閲覧が可能です。

31.8.5.2 To Build, Package, and Deploy the customer Example Using Maven

  1. GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
  2. ターミナルで以下に移動:
    tut-install/examples/jaxrs/customer/
  3. 以下のコマンドを実行。
    mvn install
    このコマンドはアプリケーションをビルドしてパッケージングしてtargetディレクトリにWARファイルを生成します。それから、GlassFish ServerにWARファイルをデプロイします。
    http://localhost:8080/customer/
    webクライアントで顧客の作成と閲覧が可能です。

関連リンク

*1:このすぐ後の文で説明されるように、CDIが「外」からインスタンスを生成可能でなければならない、という意味合い

*2:「ロストアップデート」でぐぐる

*3:https://java.net/projects/javaeetutorial/sources/svn/show/trunk/examples/jaxrs/customer

*4:https://java.net/projects/javaeetutorial/sources/svn/show/trunk/examples/jaxrs