JAX-RS Client (Eclipse Jersey)使い方メモ

# はじめに
- Jakarta RESTful Web Services の Client API の使い方のメモ
    - ここでは互換実装である Eclipse Jersey を使う
- Java EE では JAX-RS と呼ばれていたやつ
    - 正式名称は長いので、ここでは JAX-RS と表記する
- JAX-RS は、 Java で RESTful ウェブサービスを構築するための標準仕様
- 最初はサーバー側の API だけだったけど、 Java EE 7 (JAX-RS 2.0)でクライアント向けの API が増えた

# 環境
```
>gradle --version

------------------------------------------------------------
Gradle 8.8
------------------------------------------------------------

Build time:   2024-05-31 21:46:56 UTC
Revision:     4bd1b3d3fc3f31db5a26eecb416a165b8cc36082

Kotlin:       1.9.22
Groovy:       3.0.21
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          21.0.3 (Eclipse Adoptium 21.0.3+9-LTS)
OS:           Windows 10 10.0 amd64
```

# Hello World
## 検証用のサーバー
```java:PrintServlet.java
package sandbox.jaxrs.server;

import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

@WebServlet("/print/*")
public class PrintServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        try (ServletInputStream in = req.getInputStream()) {
            in.transferTo(buffer);
        }
        final Enumeration<String> names = req.getHeaderNames();
        String body = buffer.toString(StandardCharsets.UTF_8);

        System.out.printf("""
        method=%s
        requestURI=%s
        queryString=%s
        ===headers===
        %s
        ===body===
        %s
        ==========
        """, req.getMethod(), req.getRequestURI(), req.getQueryString(),
            headers(req), body);

        resp.setContentType(req.getContentType());

        try (ServletOutputStream out = resp.getOutputStream()) {
            out.write(body.getBytes(StandardCharsets.UTF_8));
        }
    }

    private String headers(HttpServletRequest req) {
        List<String> result = new ArrayList<>();

        Enumeration<String> names = req.getHeaderNames();
        while (names.hasMoreElements()) {
            String name = names.nextElement();
            Enumeration<String> headers = req.getHeaders(name);
            while (headers.hasMoreElements()) {
                String header = headers.nextElement();
                result.add(name + ": " + header);
            }
        }

        return String.join("\n", result);
    }
}
```

- 受け取ったリクエストの情報を標準出力に書き出すサーブレット
- `debug.war` という war ファイルにビルドして Tomcat にデプロイして使う
    - `http://localhost:8080/debug/print` でアクセスできる状態になる
- レスポンスボディにはリクエストボディと同じ内容を書き出す
    - `Content-Type` もリクエストと同じものをレスポンスに設定するようにしている


## 実装
```gradle:build.gradle
plugins {
    id "java"
}

compileJava.options.encoding = "UTF-8"
sourceCompatibility = 21
targetCompatibility = 21

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
}
```

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.Response;

public class JaxRsClientMain {

    public static void main(String[] args) {
        try (
            Client client = ClientBuilder.newClient();
            Response response = client.target("http://localhost:8080/debug/print")
                                      .request()
                                      .post(Entity.text("Hello World"))
        ) {
            System.out.printf("""
            status = %s
            body = %s
            """, response.getStatus(), response.readEntity(String.class));
        }
    }
}
```

```text:実行結果(サーバー出力)
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: text/plain
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 11
===body===
Hello World
==========
```

```text:実行結果(クライアント出力)
status = 200
body = Hello World
```

## 説明

```gradle:build.gradle
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
}
```

- Eclipse Jersey のクライアントを使うには、 [org.glassfish.jersey.core:jersey-client](https://mvnrepository.com/artifact/org.glassfish.jersey.core/jersey-client) を依存に追加する

```java:JaxRsClientMain.java
        try (
            Client client = ClientBuilder.newClient();
            Response response = client.target("http://localhost:8080/debug/print")
                                      .request()
                                      .post(Entity.text("Hello World"))
        ) {
            System.out.printf("""
            status = %s
            body = %s
            """, response.getStatus(), response.readEntity(String.class));
        }
```

- HTTPリクエストを送信するには `Client` インスタンスを使用する
- `ClientBuilder` で `Client` インスタンスを生成し、各種リクエストの情報を設定していく
- HTTPレスポンスは `Response` で取得でき、そこからレスポンスの情報にアクセスできる

# リクエスト送信の流れ

## Client の生成
```java
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
...

Client client = ClientBuilder.newClient();
```

- HTTPリクエストを送信するには、まず `Client` のインスタンスを用意する
- `Client` インスタンスは、 `ClientBuilder` の `newClient` メソッドで取得できる
- `Client` インスタンスの生成と破棄にはコストがかかる可能性があるので、生成するインスタンスは少数にしておくことが推奨されている
    - スレッドセーフであることが明記されてないけど、こう推奨されている以上はインスタンスは使いまわしが前提だと信じている

> Initialization as well as disposal of a Client instance may be a rather expensive operation.
> It is therefore advised to construct only a small number of Client instances in the application.
> 
> (翻訳)
> クライアント インスタンスの初期化と破棄は、かなりコストのかかる操作になる可能性があります。
> したがって、アプリケーション内に少数のクライアント インスタンスのみを構築することをお勧めします。
> 
> https://jakarta.ee/specifications/restful-ws/3.1/apidocs/jakarta.ws.rs/jakarta/ws/rs/client/client

- `Client` は `AutoCloseable` を継承しているので、不要になったらクローズが必要

## WebTarget の構築

```java
import jakarta.ws.rs.client.WebTarget;
...

WebTarget target = client.target("http://localhost:8080/debug/print");
```

- `Client` の `target` メソッドで `WebTarget` を生成する
    - `target` メソッドにはリクエストの URI を指定する
- `WebTarget` はリクエスト先の**リソース**を表すクラスとなる
- したがって、 `WebTarget` では URI を細かく構築できる

### パスを追加する

```java
WebTarget target = client.target("http://localhost:8080/debug/print")
                         .path("foo")
                         .path("/bar");

System.out.println(target);
```

```text:実行結果
JerseyWebTarget { http://localhost:8080/debug/print/foo/bar }
```

- `path` メソッドで URI にパスを追加できる

### クエリパラメータを追加する
```java
WebTarget target = client.target("http://localhost:8080/debug/print")
                         .queryParam("foo", "a", "b", "c")
                         .queryParam("bar", "あ");

System.out.println(target);
```

```text:実行結果
JerseyWebTarget { http://localhost:8080/debug/print?foo=a&foo=b&foo=c&bar=%E3%81%82 }
```

- `queryParam` でクエリパラメータを追加できる
- マルチバイト文字は自動的に URL エンコードされる
    - エンコード時の文字コードは UTF-8 固定
    - JAX-RS の仕様上では文字コードについての言及は見当たらなかった

### テンプレートを使用する

```java
WebTarget template = client.target("http://localhost:8080/")
                         .path("{foo}").path("{bar}/{foo}")
                         .queryParam("hoge", "{hoge}");

WebTarget resolved = template.resolveTemplates(Map.of(
    "foo", "FOO",
    "bar", "fizz/buzz",
    "hoge", "ほ"
));

System.out.println("template=" + template);
System.out.println("resolved=" + resolved);
```

```text:実行結果
template=JerseyWebTarget { http://localhost:8080/{foo}/{bar}/{foo}?hoge={hoge} }
resolved=JerseyWebTarget { http://localhost:8080/FOO/fizz%2Fbuzz/FOO?hoge=%E3%81%BB }
```

- パスやクエリパラメータには埋め込みパラメータを設定できる
    - パラメータは波括弧 (`{}`) でパラメータ名を囲う形で記述する
- `resolveTemplates` メソッドでパラメータに値を埋め込んだ新しい `WebTarget` を取得できる
    - 引数にはパラメータに埋め込む情報を持った `Map` を渡す
    - 元の `WebTarget` の状態は変更されない
- 埋め込まれる値は自動的に URL エンコードされる（文字コードは UTF-8）
- `resolveTemplate(String, Object)` を用いると単一のパラメータだけ解決することも可能

#### URLエンコード済みの値を埋め込む
- 埋め込む値が既に URL エンコード済みの場合は `resolveTemplatesFromEncoded` を使用する

```java:URLエンコード済みの値を埋め込みたい場合
WebTarget template =
    client.target("http://localhost:8080/{hoge}/{fuga}/{piyo}");

WebTarget resolved = template.resolveTemplatesFromEncoded(Map.of(
    "hoge", "fizz%2Fbuzz",
    "fuga", "fizz/buzz",
    "piyo", "fizz%buzz"
));

System.out.println("resolved=" + resolved);
```

```text:実行結果
resolved=JerseyWebTarget { http://localhost:8080/fizz%2Fbuzz/fizz/buzz/fizz%25buzz }
```

- `fizz%2Fbuzz` の `%2F` の部分や `fizz/buzz` の `/` の部分は本来であれば URL エンコードが必要だが、 `resolveTemplatesFromEncoded` はエンコード済みの値を渡している前提なのでエンコードは行われずそのままの形で埋め込まれるようになる
- ただし、 `fizz%buzz` の `%` のように後ろに2桁の16進数が続かない部分については明らかに URL エンコードされた値ではないので、エンコードされたうえで埋め込まれる

#### スラッシュをエンコードしないようにする
```java
WebTarget target = client.target("http://localhost:8080/{foo}/{bar}")
                         .resolveTemplate("foo", "fizz/buzz")
                         .resolveTemplate("bar", "fizz/buzz", false);

System.out.println(target);
```

```text:実行結果
JerseyWebTarget { http://localhost:8080/fizz%2Fbuzz/fizz/buzz }
```

- `boolean` のパラメータ(`encodeSlashInPath`) を受け取るメソッドが用意されており、 `false` を渡すとスラッシュの URL エンコーディングをせずにパラメータを埋め込むことができる



### URI に対しては不変

```java
WebTarget first = client.target("http://localhost:8080/debug/print");
WebTarget second = first.path("foo");
WebTarget third = first.queryParam("a", "A");

System.out.println("first =" + first);
System.out.println("second=" + second);
System.out.println("third =" + third);
```

```text:実行結果
first =JerseyWebTarget { http://localhost:8080/debug/print }
second=JerseyWebTarget { http://localhost:8080/debug/print/foo }
third =JerseyWebTarget { http://localhost:8080/debug/print?a=A }
```

- `path` や `queryParam`, `resolveTemplates` など URI に変更を加えるメソッドを実行すると、変更後の URI を持つ新しい `WebTarget` インスタンスが返される
- 元のインスタンスは変更されないので、共通部分を持った `WebTarget` を元に複数の `WebTarget` を派生させる、といった使い方ができる

### 設定に対しては可変

```java
WebTarget first = client.target("http://localhost:8080/debug/print");
WebTarget second = first.property("a", "A");
WebTarget third = second.register(new Object());

System.out.println("first.hash =" + first.hashCode());
System.out.println("second.hash=" + second.hashCode());
System.out.println("third.hash =" + third.hashCode());
```

```text:実行結果
first.hash =2114694065
second.hash=2114694065
third.hash =2114694065
```

- `property` や `register` などの設定メソッドは、インスタンスの状態を変更して同じインスタンスを返す
- 設定についての説明は後述

## Invocation の構築

```java
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.core.MediaType;
...

Invocation.Builder builder = target.request();

builder.accept(MediaType.APPLICATION_JSON)
       .header("Content-Type", "text/plain");
```

- `WebTarget` の `request` メソッドを実行すると、 `Invocation` のビルダーを取得できる
- `Invocation` は1つの HTTP リクエストを表すクラスで、ここまでで構築した URI の情報に加えヘッダーやHTTPメソッド・ボディなどの情報を持つ
- `Invocation.Builder` では、これらの情報を設定できる

### 専用のヘッダー設定メソッド

```java
import jakarta.ws.rs.core.CacheControl;
...

CacheControl cacheControl = new CacheControl();
cacheControl.setNoCache(true);

builder.accept(MediaType.APPLICATION_JSON)
       .cookie("key", "value")
       .cacheControl(cacheControl);
```

- `Accept`, `Cookie`, `Cache-Control` など、よく使われると思われるヘッダーについては専用の設定メソッドが用意されている
    - 他に `acceptEncoding`, `acceptLanguage` などもある

### 汎用のヘッダー設定メソッド
```java
builder.header("Content-Type", "text/plain");
```

- `header` メソッドを使えば任意のヘッダーを設定できる

## リクエストの実行

```java
import jakarta.ws.rs.core.Response;
...

Response response = builder.get();
```

- `Builder` にはリクエストを直接実行するためのメソッドが用意されている
    - `get`, `post`, `put`, `delete`, `options`, `head`, `trace`
    - 各メソッドは、名前が HTTP メソッドに対応している
    - 任意のHTTPメソッド指定したい場合は `method` メソッドが利用できる
- レスポンスは `Response` で返される

### ボディを指定する

```java
import jakarta.ws.rs.client.Entity;
...

Entity<String> entity = Entity.text("Hello");
Response response = builder.post(entity);
```

- ボディは `Entity` で指定する
    - `post` や `put` など、ボディを持つことができる HTTP メソッドに対応するメソッドであれば引数に `Entity` を受け取れるようになっている
- `Entity` のインスタンスは `Entity` にある `static` なファクトリメソッドで生成できる
    - `text` メソッドを使って `Entity` を作成すると、ヘッダーに `Content-Type: text/plain` が自動的に付与される
    - 同様に `json` や `xml` など、 `Content-Type` が自動的に付与されるファクトリメソッドがいくつか用意されている
        - `text`, `json`, `xml`, `html`, `xhtml`, `form` がある
    - `Content-Type` を明示的に指定したい場合は `entity` メソッドを使用する
        - `Entity.entity("Hello", MediaType.TEXT_PLAIN_TYPE)`
        - 第二引数で `MediaType` を明示的に指定できる
- ボディのシリアライズ方法の詳細については長くなるで後述

## 一旦 Invocation で受け取ってから後で実行する

```java
Invocation invocation = builder.buildGet();
Response response = invocation.invoke();
```

- `Builder` の `buildGet` などのビルドメソッドを実行すると、そこまでに構築した HTTP リクエストの情報を保持した `Invocation` のインスタンスを取得できる
    - 他に HTTP メソッドを指定できるビルドメソッドとしては、 `buildPost`, `buildPut`, `buildDelete` がある
    - それ以外の HTTP メソッドについては `build` メソッドを使用する（引数で任意の HTTP メソッドを指定できる）
- `Invocation` の状態で保持しておけば、実際の HTTP リクエストは任意のタイミングで実施できるようになる
- `invoke` メソッドを呼ぶと HTTP リクエストが実行され、レスポンスが返される
- リクエストを非同期で実行したい場合は `submit` メソッドを使用する

```java:非同期で実行する場合
Future<Response> future = invocation.submit();
```

- この場合、戻り値が `Future` になる

## レスポンスを取得する

```java
import jakarta.ws.rs.core.Response;
...

try (Response response = invocation.invoke()) {
    String body = response.readEntity(String.class);
    System.out.printf("""
    status = %s
    === body ===
    %s
    ============
    """, response.getStatus(), body);
}
```

```text:実行結果
status = 200
=== body ===
Hello World
============
```

- HTTP レスポンスは `Response` 型で受け取ることができる
- このオブジェクトからは、 HTTP レスポンスの各種情報にアクセスできる
- レスポンスボディの情報を取り出すには `readEntity` メソッドを使用する
    - 引数にはボディの情報を格納する Java の型を指定する
    - ボディのデシリアライズに関しての詳細は後述
- `Response` は `AutoCloseable` を継承している
    - 一応 `Response` は、 `readEntity` などでエンティティの情報を読み取ると自動的にクローズされることになっている
    - ただし、エンティティを `InputStream` で受け取った場合は明示的なクローズが必要となる
    - ややこしいので、事故らないようにするためにも `Response` は常に try-with-resources に入れてクローズするように実装するのが良いのではないかと個人的に思う

### 直接エンティティだけを取得する

```java
String body = builder.post(Entity.text("Hello World"), String.class);
System.out.println("body=" + body);
```

```text:実行結果
body=Hello World
```

- `Invocation.Builder` の直接 HTTP リクエストを実行する `post` などのメソッドや `Invocation` の `invoke` などのメソッドには、レスポンスのエンティティの型を引数で指定できるようになっている
- これを指定すると、戻り値の型は `Response` ではなく、引数で指定したエンティティの型になり直接エンティティだけを受け取ることができるようになる
    - 要するに、いきなり `Response` の `readEntity` を呼んでいる感じになる
- レスポンスのステータスコードが 2xx 系でない場合は例外がスローされる

---


# ボディ（エンティティ）のシリアライズ・デシリアライズ

ここでは HTTP メッセージのボディ（エンティティ）のシリアライズとデシリアライズがどのような仕組みで制御されているのか、どう調整できるのかについてまとめる。

## エンティティプロバイダ
- リクエストおよびレスポンスボディ（エンティティ）のシリアライズ・デシリアライズは、**エンティティプロバイダ**によって行われる
- エンティティプロバイダは `MessageBodyWriter` および `MessageBodyReader` インタフェースを実装することで作成できる
- それぞれのインタフェースの定義は以下のようになっている

```java:MessageBodyWriter
package jakarta.ws.rs.ext;

public interface MessageBodyWriter<T> {

    public boolean isWriteable(
        Class<?> type,
        Type genericType,
        Annotation[] annotations,
        MediaType mediaType);

    // ★ getSize メソッドは実装不要（詳細後述）
    public default long getSize(
        final T t,
        final Class<?> type,
        final Type genericType,
        final Annotation[] annotations,
        final MediaType mediaType) {
        return -1L;
    }
    
    public void writeTo(
        T t,
        Class<?> type,
        Type genericType,
        Annotation[] annotations,
        MediaType mediaType,
        MultivaluedMap<String, Object> httpHeaders,
        OutputStream entityStream)
            throws java.io.IOException, jakarta.ws.rs.WebApplicationException;
}
```

```java:MessageBodyReader
package jakarta.ws.rs.ext;

public interface MessageBodyReader<T> {

    public boolean isReadable(
        Class<?> type,
        Type genericType,
        Annotation[] annotations,
        MediaType mediaType);

    public T readFrom(
        Class<T> type,
        Type genericType,
        Annotation[] annotations,
        MediaType mediaType,
        MultivaluedMap<String, String> httpHeaders,
        InputStream entityStream)
            throws java.io.IOException, jakarta.ws.rs.WebApplicationException;
}
```

- これらのインタフェースを実装したエンティティプロバイダを JAX-RS のランタイムに登録しておく
- すると、エンティティごとに適したエンティティプロバイダが選択され、シリアライズ・デシリアライズが行われる
- 大雑把な流れを以下に示す

```plantuml
participant "アプリ" as app
participant Invocation as inv
participant "JAX-RSランタイム" as runtime
participant MessageBodyWriter as writer
participant MessageBodyReader as reader

app -> inv: invoke()
inv -> runtime
loop 登録されている各MessageBodyWriter
    runtime -> writer: isWritable()
    alt isWritable() == true
        runtime -> writer: writeTo()
    end
end

== <リクエストの実行> ==

loop 登録されている各MessageBodyReader
    runtime -> reader: isReadable()
    alt isReadable() == true
        runtime -> reader: readFrom()
    end
end
```

- JAX-RS のランタイムは、エンティティを書き出すときにまずは登録されている各 `MessageBodyWriter` の `isWritable` を呼び出す
    - `isWritable` には、書き出そうとしているエンティティの型(`Class`)やメディアタイプなどの情報が渡される
    - `isWritable` は渡された情報をもとに、そのエンティティのシリアライズに対応しているかどうかを判定し `boolean` で結果を返す(対応している場合は `true`)
    - JAX-RS のランタイムは、最初に `true` を返した `MessageBodyWriter` の `writeTo` メソッドを使ってエンティティの書き出しを行う
- 読み取りの場合も同様で、登録されている各 `MessageBodyReader` の `isReadable` が呼ばれる
    - `isReadable` にはデシリアライズ後のエンティティの型やメディアタイプなどが渡されるので、デシリアライズに対応している場合は `true` を返すように実装する
    - JAX-RS ランタイムは最初に `true` を返した `MessageBodyReader` の `readFrom` メソッドを使ってエンティティのデシリアライズを行う

:::note
**`MessageBodyWriter` の `getSize` メソッドは実装不要**

`MessageBodyWriter` の `getSize` メソッドは、 JAX-RS 2.0 では無視されるようになっている。
`Content-Length` ヘッダーに設定する値の計算は JAX-RS のランタイムが行うようになっている。
:::



## エンティティプロバイダの登録方法

### 実装

```java:MyLocalDateEntityProvider
package sandbox.jaxrs.provider;

import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.MessageBodyWriter;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class MyLocalDateEntityProvider implements
        MessageBodyWriter<LocalDate>, MessageBodyReader<LocalDate> {
    private static final DateTimeFormatter FORMATTER
            = DateTimeFormatter.ofPattern("uuuu/MM/dd");

    @Override
    public boolean isWriteable(
            Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType) {
        return type == LocalDate.class;
    }

    @Override
    public void writeTo(
            LocalDate entity, Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders,
            OutputStream entityStream) throws IOException, WebApplicationException {

        byte[] bytes = FORMATTER.format(entity).getBytes(StandardCharsets.UTF_8);
        entityStream.write(bytes);
        entityStream.flush();
    }

    @Override
    public boolean isReadable(
            Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType) {
        return type == LocalDate.class;
    }

    @Override
    public LocalDate readFrom(
            Class<LocalDate> type, Type genericType, Annotation[] annotations,
            MediaType mediaType, MultivaluedMap<String, String> httpHeaders,
            InputStream entityStream) throws IOException, WebApplicationException {
        byte[] buffer = new byte[10];
        entityStream.read(buffer);
        String text = new String(buffer, StandardCharsets.UTF_8);
        return LocalDate.parse(text, FORMATTER);
    }
}
```

- `LocalDate` を `yyyy/MM/dd` 形式の文字列にシリアライズ・デシリアライズするエンティティプロバイダ

```java:JaxRsClientMain
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.MyLocalDateEntityProvider;

import java.time.LocalDate;

public class JaxRsClientMain {

    public static void main(String[] args) {
        LocalDate result = ClientBuilder.newClient()
                            .register(MyLocalDateEntityProvider.class)
                            .target("http://localhost:8080/debug/print")
                            .request()
                            .post(Entity.text(LocalDate.now()), LocalDate.class);

        System.out.println(result);
    }
}
```

### 実行結果

```text:サーバー側出力
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: text/plain
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 10
===body===
2024/09/17
==========
```

```text:クライアント側出力
2024-09-17
```

### 説明

```java:JaxRsClientMain
        LocalDate result = ClientBuilder.newClient()
                            .register(MyLocalDateEntityProvider.class)
```

- エンティティプロバイダの登録は、 `Client` などが提供している `register` メソッドを使用して行う
- 引数にプロバイダの `Class` オブジェクトを渡すことで登録できる
- `register` メソッドは、実際には `Configurable` インタフェースで定義されており、 `Client` はこれを継承している関係になっている
- 他にも `WebTarget` も `Configurable` を継承しているので、こちらでもプロバイダの登録ができる

```plantuml
scale 0.7

interface Configurable<C> {
  register(componentClass: Class<?>): C
}
class ClientBuilder implements Configurable
interface Client extends Configurable
interface WebTarget extends Configurable
class ClientConfig implements Configurable
```

## メディアタイプによる候補の絞り込み

### 実装
```java:MyLocalDateJsonEntityProvider.java
package sandbox.jaxrs.provider;

import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyWriter;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

@Produces(MediaType.APPLICATION_JSON)
public class MyLocalDateJsonEntityProvider implements MessageBodyWriter<LocalDate> {
    private static final DateTimeFormatter FORMATTER
            = DateTimeFormatter.ofPattern("uuuu/MM/dd");

    @Override
    public boolean isWriteable(
            Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType) {
        System.out.println("MyLocalDateJsonEntityProvider.isWritable");
        return type == LocalDate.class;
    }

    @Override
    public void writeTo(
            LocalDate localDate, Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
            throws IOException, WebApplicationException {
        String json = """
        {"date": "%s"}
        """.formatted(FORMATTER.format(localDate));

        entityStream.write(json.getBytes(StandardCharsets.UTF_8));
        entityStream.flush();
    }
}
```

- `LocalDate` を受け取って簡単な JSON に変換するエンティティプロバイダ（`MessageBodyWriter` のみ実装）
- `isWritable` が呼ばれた標準出力にメッセージを出力するようにしている

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.WebTarget;
import sandbox.jaxrs.provider.MyLocalDateEntityProvider;
import sandbox.jaxrs.provider.MyLocalDateJsonEntityProvider;

import java.time.LocalDate;

public class JaxRsClientMain {

    public static void main(String[] args) {
        WebTarget target = ClientBuilder.newClient()
                            .register(MyLocalDateEntityProvider.class)
                            .register(MyLocalDateJsonEntityProvider.class)
                            .target("http://localhost:8080/debug/print");

        System.out.println("post as application/json");
        target.request().post(Entity.json(LocalDate.now()));

        System.out.println("post as plain/text");
        target.request().post(Entity.text(LocalDate.now()));
    }
}
```

- エンティティプロバイダとして `MyLocalDateEntityProvider` と `MyLocalDateJsonEntityProvider` の2つを登録
- 最初のリクエストは `Entity.json()` を使って `Content-Type` を `application/json` にして送信
- ２つ目のリクエストは `Entity.text()` を使って `plain/text` で送信


### 実行結果

```text:クライアント側の出力
post as application/json
MyLocalDateJsonEntityProvider.isWritable
post as plain/text
```

```text:サーバー側の出力(一部省略)
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/json
...
===body===
{"date": "2024/09/18"}

==========
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: text/plain
...
===body===
2024/09/18
==========
```

### 説明

```java:MyLocalDateJsonEntityProvider.java
@Produces(MediaType.APPLICATION_JSON)
public class MyLocalDateJsonEntityProvider implements MessageBodyWriter<LocalDate> {
```

- `MessageBodyWriter` を実装したエンティティプロバイダに `Produces` アノテーションを付けることで、そのエンティティプロバイダを適用するメディアタイプを限定することができる
- この場合、 `MediaType.APPLICATION_JSON` を設定しているので、 `MyLocalDateJsonEntityProvider` はメディアタイプが `application/json` の場合のみ候補として選択されるようになる
    - `Entity.json()` で作ったリクエストでは `isWritable` が呼ばれているが、 `Entity.text()` で作ったリクエストでは呼ばれなくなっている（候補にもならなくなっている）
- これにより、エンティティプロバイダの候補選びで無駄な `isWritable` の呼び出しを減らすことができるようになる
- `MessageBodyReader` を実装したエンティティプロバイダの候補を絞るには `Consumes` アノテーションを使用する


|インタフェース|アノテーション|
|---|---|
|`MessageBodyWriter`|`Produces`|
|`MessageBodyReader`|`Consumes`|

## デフォルトで提供されているエンティティプロバイダ

- JAX-RS の仕様として、以下の型・メディアタイプの組み合わせはサポートされていることが保証されている

|エンティティの型|メディアタイプ|
|---|---|
|`byte[]`|`*/*`|
|`java.lang.String`|`*/*`|
|`java.io.InputStream`|`*/*`|
|`java.io.Reader`|`*/*`|
|`java.io.File`|`*/*`|
|`jakarta.activation.DataSource`|`*/*`|
|`javax.xml.transform.Source`|`text/xml`<br>`application/xml`<br>`applicaiton/*+xml`|
|`jakarta.ws.rs.core.MultivaluedMap`|`application/x-www-form-urlencoded`|
|`java.util.List<EntityPart>`|`multipart/form-data`|
|`jakarta.ws.rs.core.StreamingOutput`|`*/*`|
|`java.lang.Boolean`|`text/plain`|
|`java.lang.Character`|`text/plain`|
|`java.lang.Number`|`text/plain`|

- `*/*` は任意のメディアタイプを表している
- プリミティブ型については、対応するラッパークラスにオートボクシングされることで処理される
- なお、アプリケーションが登録したエンティティプロバイダは、組み込みのエンティティプロバイダよりも優先される

## Jersey が提供しているよく使うエンティティプロバイダ

XML や JSON など、よく利用されるコンテンツタイプのエンティティプロバイダは Jersey によってあらかじめプラグインが用意されている。

### JAXB (XML)

**実装**

```gradle:build.gradle
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓を追加
    implementation "org.glassfish.jersey.media:jersey-media-jaxb:3.1.8"
    implementation "com.sun.xml.bind:jaxb-impl:4.0.5"
}
```

- JAXB で XML の変換を行えるようにするには、以下2つを追加する
    - Jersey の JAXB 用モジュール([org.glassfish.jersey.media:jersey-media-jaxb](https://mvnrepository.com/artifact/org.glassfish.jersey.media/jersey-media-jaxb))
    - JAXB の互換実装([com.sun.xml.bind:jaxb-impl](https://mvnrepository.com/artifact/com.sun.xml.bind/jaxb-impl))

```java:MyEntity.java
package sandbox.jaxrs.client;

import jakarta.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class MyEntity {
    private int id;

    public MyEntity() {}

    public MyEntity(int id) {
        this.id = id;
    }

    // getter, setter, toString 省略
}
```

- `@XmlRootElement` でクラスを注釈しておく

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;

public class JaxRsClientMain {

    public static void main(String[] args) {
        MyEntity entity = new MyEntity(1);

        MyEntity response = ClientBuilder.newClient()
                            .target("http://localhost:8080/debug/print")
                            .request()
                            .post(Entity.xml(entity), MyEntity.class);

        System.out.println(response);
    }
}
```

- `Entity.xml()` でエンティティを設定してリクエストを投げる

**実行結果**

```text:実行結果(サーバー側)
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/xml
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 86
===body===
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><myEntity><id>1</id></myEntity>
==========
```

- `MyEntity` の内容が XML にシリアライズされて送信できている

```text:実行結果(クライアント側)
MyEntity{id=1}
```

- レスポンスの XML もデシリアライズされて取得できている

**説明**

```java:MyEntity.java
@XmlRootElement
public class MyEntity {
```

- JAXB で処理されるには、エンティティが `@XmlRootElement` で注釈されている必要がある

#### XmlRootElement でエンティティを注釈できない場合

- 変換したいエンティティを `@XmlRootElement` で注釈できない場合は、 `JAXBElement` を使用する

**実装**

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.GenericType;
import jakarta.xml.bind.JAXBElement;

import javax.xml.namespace.QName;

public class JaxRsClientMain {

    public static void main(String[] args) {
        MyEntity entity = new MyEntity(1);
        JAXBElement<MyEntity> jaxbEntity
            = new JAXBElement<>(new QName("entity"), MyEntity.class, entity);

        JAXBElement<MyEntity> responseElement = ClientBuilder.newClient()
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.xml(jaxbEntity), new GenericType<>() {});
        MyEntity response = responseElement.getValue();

        System.out.println(response);
    }
}
```

**実行結果**

```text:実行結果(サーバー側)
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/xml
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 82
===body===
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><entity><id>1</id></entity>
==========
```

```text:実行結果(クライアント側)
MyEntity{id=1}
```

**説明**

```java:JaxRsClientMain
        MyEntity entity = new MyEntity(1);
        JAXBElement<MyEntity> jaxbEntity
            = new JAXBElement<>(new QName("entity"), MyEntity.class, entity);

            ...
                .post(Entity.xml(jaxbEntity), ...);
```

- エンティティに `XmlRootElement` を付けられない場合は、 `JAXBElement` にエンティティを入れることでアノテーションなしで変換ができるようになる
- `JAXBElement` のコンストラクタには、以下を渡す
    - 第一引数には `QName` を指定する
        - この値は、 XML のルートタグの名前として利用される
    - 第二引数には、エンティティの `Class` オブジェクトを渡す
    - 第三引数には、変換対象のエンティティ自身を渡す
- `JAXBElement` を `Entity` に詰めてボディに設定する

```java:JaxRsClientMain.java
        JAXBElement<MyEntity> responseElement = ClientBuilder.newClient()
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.xml(jaxbEntity), new GenericType<>() {});
```

- レスポンスは `GenericType` を使用し、同じく `JAXBElement` で受け取る

### JSON-B (JSON)

**実装**

```gradle:build.gradle
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓を追加
    implementation "org.glassfish.jersey.media:jersey-media-json-binding:3.1.8"
}
```

```java:MyEntity.java
package sandbox.jaxrs.client;

public class MyEntity {
    private int id;

    public MyEntity() {}

    // getter, setter, toString は省略
}
```

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;

public class JaxRsClientMain {

    public static void main(String[] args) {
        MyEntity entity = new MyEntity(1);

        MyEntity response = ClientBuilder.newClient()
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.json(entity), MyEntity.class);

        System.out.println(response);
    }
}
```

**実行結果**

```text:実行結果(サーバー側)
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/json
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 8
===body===
{"id":1}
==========
```

```text:実行結果(クライアント側)
MyEntity{id=1}
```

**説明**

```gradle:build.gradle
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓を追加
    implementation "org.glassfish.jersey.media:jersey-media-json-binding:3.1.8"
}
```

- Jackson で JSON のシリアライズ・デシリアライズを行いたい場合は、 [org.glassfish.jersey.media:jersey-media-json-binding](https://mvnrepository.com/artifact/org.glassfish.jersey.media/jersey-media-json-binding) を依存関係に追加する
- 依存関係を追加したら、それだけで JSON-B 用のエンティティプロバイダを使い始められる
    - 実装には [Eclipse Yasson](https://github.com/eclipse-ee4j/yasson) が使用される
    - サービスローダーの仕組みで JSON-B のエンティティプロバイダが自動的に有功化されるようになっている

#### Jsonb を調整する

- `Jsonb` を調整したい場合は以下のようにする

**実装**

```java:MyJsonbConfigProvider.java
package sandbox.jaxrs.provider;

import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
import jakarta.ws.rs.ext.ContextResolver;

public class MyJsonbConfigProvider implements ContextResolver<Jsonb> {
    @Override
    public Jsonb getContext(Class<?> aClass) {
        JsonbConfig config = new JsonbConfig();
        config.withFormatting(true);
        return JsonbBuilder.create(config);
    }
}
```

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.MyJsonbConfigProvider;

public class JaxRsClientMain {

    public static void main(String[] args) {
        MyEntity entity = new MyEntity(1);

        MyEntity response = ClientBuilder.newClient()
                .register(MyJsonbConfigProvider.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.json(entity), MyEntity.class);

        System.out.println(response);
    }
}
```

**実行結果**

```text:実行結果(サーバー側)
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/json
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 15
===body===
{
    "id": 1
}
==========
```

**説明**

```java:MyJsonbConfigProvider.java
public class MyJsonbConfigProvider implements ContextResolver<Jsonb> {
    @Override
    public Jsonb getContext(Class<?> aClass) {
        JsonbConfig config = new JsonbConfig();
        config.withFormatting(true);
        return JsonbBuilder.create(config);
    }
}
```

- `Jsonb` を調整したい場合は `ContextResolver` インタフェースを実装したクラスを作り、 `getContext` でカスタマイズした `Jsonb` を返すように実装する

```java:JaxRsClientMain.java
        MyEntity response = ClientBuilder.newClient()
                .register(MyJsonbConfigProvider.class)
```

- 作成した自作の `ContextResolver` を `register` で登録することで有効化できる



### Jackson (JSON)

**実装**

```gradle:build.gradle
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓を追加
    implementation "org.glassfish.jersey.media:jersey-media-json-jackson:3.1.8"
}
```

```java:MyEntity.java
package sandbox.jaxrs.client;

public class MyEntity {
    private int id;

    public MyEntity() {}

    // getter, setter, toString は省略
}
```

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;

public class JaxRsClientMain {

    public static void main(String[] args) {
        MyEntity entity = new MyEntity(1);

        MyEntity response = ClientBuilder.newClient()
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.json(entity), MyEntity.class);

        System.out.println(response);
    }
}
```

**実行結果**

```text:実行結果(サーバー側)
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/json
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 8
===body===
{"id":1}
==========
```

- JSON にシリアライズされて送信できている

```text:実行結果(クライアント側)
MyEntity{id=1}
```

- JSON から `MyEntity` にデシリアライズできている

**説明**

```gradle:build.gradle
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓を追加
    implementation "org.glassfish.jersey.media:jersey-media-json-jackson:3.1.8"
}
```

- Jackson で JSON のシリアライズ・デシリアライズを行いたい場合は、 [org.glassfish.jersey.media:jersey-media-json-jackson](https://mvnrepository.com/artifact/org.glassfish.jersey.media/jersey-media-json-jackson) を依存関係に追加する
- 依存関係を追加したら、それだけで Jackson 用のエンティティプロバイダを使い始められる
    - サービスローダーの仕組みで Jackson のエンティティプロバイダが自動的に有功化されるようになっている

#### ObjectMapper を調整する

- `ObjectMapper` を調整したい場合は以下のようにする

**実装**

```java:MyObjectMapperProvider.java
package sandbox.jaxrs.provider;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import jakarta.ws.rs.ext.ContextResolver;

public class MyObjectMapperProvider implements ContextResolver<ObjectMapper> {

    private final ObjectMapper objectMapper;

    public MyObjectMapperProvider() {
        objectMapper = new ObjectMapper();
        objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return objectMapper;
    }
}
```

- `ContextResolver` インタフェースを実装したクラスを作成
- `SerializationFeature.INDENT_OUTPUT` を有効化してシリアライズ時に JSON をフォーマットするように設定
- `getContext` でカスタマイズした `ObjectMapper` を返すように実装

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.MyObjectMapperProvider;

public class JaxRsClientMain {

    public static void main(String[] args) {
        MyEntity entity = new MyEntity(1);

        MyEntity response = ClientBuilder.newClient()
                .register(MyObjectMapperProvider.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.json(entity), MyEntity.class);

        System.out.println(response);
    }
}
```

- `register` で `MyObjectMapperProvider` を登録するように設定

**実行結果**

```text:実行結果(サーバー側)
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/json
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 16
===body===
{
  "id" : 1
}
==========
```

- JSON がフォーマットされている

**説明**

```java:MyObjectMapperProvider.java
public class MyObjectMapperProvider implements ContextResolver<ObjectMapper> {

    private final ObjectMapper objectMapper;

    public MyObjectMapperProvider() {
        objectMapper = new ObjectMapper();
        objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return objectMapper;
    }
}
```

- `ObjectMapper` を調整したい場合は `ContextResolver` インタフェースを実装したクラスを作り、 `getContext` でカスタマイズした `ObjectMapper` を返すように実装する

```java:JaxRsClientMain.java
        MyEntity response = ClientBuilder.newClient()
                .register(MyObjectMapperProvider.class)
```

- 作成した自作の `ContextResolver` を `register` で登録することで有効化できる


## Jakarta EE 環境だと標準でサポートされているエンティティプロバイダ

- Jakarta EE 環境だと、 JSON や XML のエンティティプロバイダがデフォルトでサポートされるようになっている

**実装**

```text:フォルダ構成
|-build.gradle
`-src/main/java/
  `-sandbox/jaxrs/
    |-MyApplication.java
    |-HelloResource.java
    `-MyEntity.java
```

```gradle:build.gradle
plugins {
    id "war"
}

sourceCompatibility = 21
targetCompatibility = 21
compileJava.options.encoding = "UTF-8"

repositories {
    mavenCentral()
}

dependencies {
    compileOnly "jakarta.ws.rs:jakarta.ws.rs-api:3.1.0"
    compileOnly "jakarta.xml.bind:jakarta.xml.bind-api:4.0.2"
}

war.archiveBaseName = "jaxrs"
```

- JAX-RS と JAXB を依存に追加した war プロジェクトの設定
- ビルドすると `jaxrs.war` という war が生成されるように設定

```java:MyApplication.java
package sandbox.jaxrs;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("/api")
public class MyApplication extends Application {
}
```

- `@ApplicationPath` の設定
- パスのルートを `/api` で定義

```java:MyEntity.java
package sandbox.jaxrs;

import jakarta.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class MyEntity {
    public int id;
}
```

- ボディに使用するエンティティ
- `@XmlRootElement` で注釈して JAXB のエンティティプロバイダの処理対象となるように定義

```java:HelloResource.java
package sandbox.jaxrs;

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.MediaType;

@Path("/hello")
public class HelloResource {

    @GET
    public void hello() {
        try (Client client = ClientBuilder.newClient()) {
            WebTarget target =
                client.target("http://localhost:8080/jaxrs/api/hello/world");

            MyEntity entity = new MyEntity();
            entity.id = 10;

            target.request().post(Entity.json(entity)).close();
            target.request().post(Entity.xml(entity)).close();
        }
    }

    @POST
    @Path("/world")
    @Consumes(MediaType.APPLICATION_JSON)
    public void worldJson(String entity) {
        System.out.println("== application/json ==");
        System.out.println(entity);
    }

    @POST
    @Path("/world")
    @Consumes(MediaType.APPLICATION_XML)
    public void worldXml(String entity) {
        System.out.println("== application/xml ==");
        System.out.println(entity);
    }
}
```

- JAX-RS のクライアント兼サーバーの実装
- `GET /api/hello` でリクエストを受け付け、 JAX-RS Client API を利用して `POST /api/hello/world` で2つのリクエストを送信
    - 1つ目は `MyEntity` を JSON にして送信
    - 2つ目は `MyEntity` を XML にして送信
- JSON と XML のそれぞれで受け取れるリソースメソッドを定義し、ボディの内容を標準出力している

**実行結果**

```text:実行結果
== application/json ==
{"id":10}
== application/xml ==
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><myEntity><id>10</id></myEntity>
```

※WildFly 33.0.2.Final にデプロイして実行

**説明**

- JAXB および JSON-B をサポートしている環境（Jakarta EE サーバー環境）では、それぞれのエンティティプロバイダがデフォルトで有効になっている
- このため、依存関係などを追加しなくてもそのまま XML, JSON の変換ができる

# 設定

```plantuml
scale 0.7

interface Configurable<C> {
  property(String name, Object value): C
  register(componentClass: Class<?>): C
}
class ClientBuilder implements Configurable
interface Client extends Configurable
interface WebTarget extends Configurable
class ClientConfig implements Configurable
```

- 設定や拡張機能を追加できる API として `Configurable` というインタフェースが用意されている
- `ClientBuilder` や `Client`, `WebTarget` などはこの `Configurable` を継承または実装しているため、同じ方法で設定が可能となっている
- `Configurable` を介して設定できるモノには以下の3つがある
    1. プロパティ(Properties)
        - キー・バリューの設定
    2. フィーチャー(Features)
        - 複数のプロバイダやプロパティをまとめて設定するための仕組み
        - `Feature` インタフェースを実装して作成する
    3. プロバイダ(Providers)
        - 様々な機能追加や拡張を提供するものの総称
        - 前述のエンティティとのマッピング機能を提供するエンティティプロバイダもこの１つ

## プロバイダ(Providers)
### エンティティプロバイダ(Entity Providers)

[エンティティプロバイダ](#エンティティプロバイダ) を参照。

### コンテキストプロバイダ(Context Providers)

- 他のプロバイダに対してコンテキストを提供するプロバイダ
- 例えば Jackson のエンティティプロバイダに対して `ObjectMapper` のインスタンスを提供したり、 JSON-B のエンティティプロバイダに `Jsonb` のインスタンスを提供するプロバイダ
- `ContextResolver` インタフェースを実装して作成する

### フィルター(Filters)

- フィルターを使うと、リクエストの送信前後にリクエストやレスポンスの情報を書き換えることができる
- これにより、共通のヘッダーを設定したり、レスポンスの情報をログに出力するといったことができるようになる
- フィルターは、以下のいずれかのインタフェースを実装して作成する
    - `ClientRequestFilter`
    - `ClientResponseFilter`
    - `ContainerRequestFilter`
    - `ContainerResponseFilter`
- `Container*Filter` はサーバー側で使うものなのでここでは割愛

**実装**

```java:MyFilter.java
package sandbox.jaxrs.provider.filter;

import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.client.ClientResponseContext;
import jakarta.ws.rs.client.ClientResponseFilter;
import jakarta.ws.rs.core.MultivaluedMap;

public class MyFilter implements ClientRequestFilter, ClientResponseFilter {
    @Override
    public void filter(ClientRequestContext requestContext) {
        MultivaluedMap<String, Object> headers = requestContext.getHeaders();
        headers.add("X-Test-Header", "TestValue");
    }

    @Override
    public void filter(ClientRequestContext requestContext,
                       ClientResponseContext responseContext) {
        int length = responseContext.getLength();
        System.out.println("response length = " + length);
    }
}
```

- `ClientRequestFilter` および `ClientResponseFilter` インタフェースを実装している
- `ClientRequestFilter` の `filter` メソッドでは、ヘッダーに `X-Test-Header` というのを追加している
- `ClientResponseFilter` の `filter` メソッドでは、レスポンスボディの長さを取得して標準出力に出力している

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.filter.MyFilter;

public class JaxRsClientMain {

    public static void main(String[] args) {
        String response = ClientBuilder.newClient()
                .register(MyFilter.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.text("Hello World"), String.class);

        System.out.println(response);
    }
}
```

- `register` メソッドで `MyFilter` を登録して実行している

**実行結果**

```text:実行結果(サーバー側出力)
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: text/plain
x-test-header: TestValue
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 11
===body===
Hello World
==========
```

- ヘッダーに `x-test-header` が追加されているのが分かる

```text:実行結果(クライアント側出力)
response length = 11
Hello World
```

- 長さの情報が出力されている

**説明**

```java:MyFilter.java
public class MyFilter implements ClientRequestFilter, ClientResponseFilter {
    @Override
    public void filter(ClientRequestContext requestContext) {
    ...

    @Override
    public void filter(ClientRequestContext requestContext,
                       ClientResponseContext responseContext) {
   ...
}
```

- `ClientRequestFilter` インタフェースを実装すると、リクエストが送信される前に処理を挟むことができる
    - `ClientRequestContext` だけを受け取る `filter` メソッドを実装する
    - `ClientRequestContext` には送信しようとしているリクエストの情報が格納されており、このインスタンスの状態を書き換えることでリクエストの内容を変更できる
- `ClientResponseFilter` インタフェースを実装すると、レスポンスを受信した後に処理を挟むことができる
    - `ClientRequestContext` と `ClientResponseContext` を受け取る `filter` メソッドを実装する
    - `ClientResponseContext` からは、受信したレスポンスの情報を取得できる
    - レスポンスの内容の書き換えも可能

```java:JaxRsClientMain.java
        String response = ClientBuilder.newClient()
                .register(MyFilter.class)
```

- 作成したフィルターは、 `Configurable` の `register` メソッドに `Class` オブジェクトを渡すことで登録できる

#### 処理を中断する

```java:MyAbortFilter.java
package sandbox.jaxrs.provider.filter;

import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

public class MyAbortFilter implements ClientRequestFilter {
    @Override
    public void filter(ClientRequestContext requestContext) {
        Response response
            = Response.ok("abort!", MediaType.TEXT_PLAIN_TYPE).build();
        requestContext.abortWith(response);
    }
}
```

```text:実行結果(クライアント側出力)
abort!
```

- `ClientRequestContext` の `abortWith` メソッドを呼ぶと、リクエストの処理が中断される
- `abortWith` の引数には、クライアントに返すレスポンスを渡す必要がある
- これは、例えばキャッシュがある場合は実際にはリクエストは投げずにキャッシュのレスポンスを返す、みたいな機能を実現したいときに利用できる

#### プロパティを受け取る

**実装**

```java:MyPropertiesFilter.java
package sandbox.jaxrs.provider.filter;

import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;

public class MyPropertiesFilter implements ClientRequestFilter {
    @Override
    public void filter(ClientRequestContext requestContext) {
        for (String name : requestContext.getPropertyNames()) {
            System.out.println(name + ": " + requestContext.getProperty(name));
        }
    }
}
```

- `ClientRequestContext` から `getProperty` で取得できるプロパティの値を全て出力している

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import sandbox.jaxrs.provider.filter.MyPropertiesFilter;

public class JaxRsClientMain {

    public static void main(String[] args) {
        ClientBuilder.newClient()
                .property("foo", "FOO@Client")
                .register(MyPropertiesFilter.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .property("hoge", "HOGE@Invocation.Builder")
                .buildGet()
                .property("fuga", "FUGA@Invocation")
                .invoke()
                .close();
    }
}
```

- `Client` と `Invocation.Builder`, `Invocation` でそれぞれプロパティを設定している

**実行結果**

```text:実行結果(クライアント側出力)
hoge: HOGE@Invocation.Builder
fuga: FUGA@Invocation
```

- `Invocation.Builder` と `Invocation` で設定したプロパティだけが出力されている
- `Client` で設定したプロパティは出力されていない

**説明**

```java:MyPropertiesFilter.java
    public void filter(ClientRequestContext requestContext) {
        for (String name : requestContext.getPropertyNames()) {
            System.out.println(name + ": " + requestContext.getProperty(name));
        }
    }
```

- `ClientRequestContext` からは、現在のリクエストコンテキストのプロパティにアクセスできる
- リクエストコンテキストのプロパティは、 `Invocation.Builder` や `Invocation `の `property` で設定できる
- `Client` の `property` (= `Configurable` で設定されたプロパティ)の値は参照できない
    - `Configurable` で設定したプロパティは、 `Configuration` から参照できる
    - 詳しくは [フィーチャー](#フィーチャーfeatures) にて


### インターセプター(Interceptors)

- インターセプターを使うと、エンティティの読み書きのときに処理をラップできる
- インターセプタは、以下のいずれかのインタフェースを実装することで作成する
    - `ReaderInterceptor`
    - `WriterInterceptor`

**実装**

```java:MyInterceptor.java
package sandbox.jaxrs.provider.interceptor;

import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.ext.ReaderInterceptor;
import jakarta.ws.rs.ext.ReaderInterceptorContext;
import jakarta.ws.rs.ext.WriterInterceptor;
import jakarta.ws.rs.ext.WriterInterceptorContext;

import java.io.IOException;

public class MyInterceptor implements ReaderInterceptor, WriterInterceptor {
    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context)
            throws IOException, WebApplicationException {
        try {
            System.out.println("[aroundReadFrom] before");
            return context.proceed();
        } finally {
            System.out.println("[aroundReadFrom] after");
        }
    }

    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
            throws IOException, WebApplicationException {
        System.out.println("[aroundWriteTo] before");
        context.proceed();
        System.out.println("[aroundWriteTo] after");
    }
}
```

- `ReaderInterceptor` と `WriterInterceptor` を実装し、後続処理の呼び出し前後にログを出力している

```java:MySimpleEntityProvider.java
package sandbox.jaxrs.provider;

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.MessageBodyWriter;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;

@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.TEXT_PLAIN)
public class MySimpleEntityProvider implements
        MessageBodyReader<String>, MessageBodyWriter<String> {
    @Override
    public boolean isReadable(Class type,
                              Type genericType,
                              Annotation[] annotations,
                              MediaType mediaType) {
        return type == String.class;
    }

    @Override
    public String readFrom(Class type,
                           Type genericType,
                           Annotation[] annotations,
                           MediaType mediaType,
                           MultivaluedMap httpHeaders,
                           InputStream entityStream)
            throws IOException, WebApplicationException {
        System.out.println("readFrom");
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            entityStream.transferTo(out);
            return out.toString(StandardCharsets.UTF_8);
        }
    }

    @Override
    public boolean isWriteable(Class type,
                               Type genericType,
                               Annotation[] annotations,
                               MediaType mediaType) {
        return type == String.class;
    }

    @Override
    public void writeTo(String o,
                        Class type,
                        Type genericType,
                        Annotation[] annotations,
                        MediaType mediaType,
                        MultivaluedMap httpHeaders,
                        OutputStream entityStream)
            throws IOException, WebApplicationException {
        System.out.println("writeTo");
        try (OutputStreamWriter writer
                = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8)) {
            writer.write(o);
        }
    }
}
```

- `String` をそのまま書き出すだけの簡単なエンティティプロバイダ
- 動作を見るために、各メソッド呼び出し時にログを出力している

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.MySimpleEntityProvider;
import sandbox.jaxrs.provider.interceptor.MyInterceptor;

public class JaxRsClientMain {

    public static void main(String[] args) {
        String response = ClientBuilder.newClient()
                .register(MySimpleEntityProvider.class)
                .register(MyInterceptor.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.text("Hello World"), String.class);

        System.out.println(response);
    }
}
```

- `MySimpleEntityProvider` と `MyInterceptor` の両方を登録してリクエストを送信している

**実行結果**

```text:実行結果(クライアント側出力)
[aroundWriteTo] before
writeTo
[aroundWriteTo] after
[aroundReadFrom] before
readFrom
[aroundReadFrom] after
Hello World
```

- エンティティプロバイダのログの前後でインターセプターのログが出力されている

**説明**

```java:MyInterceptor.java
public class MyInterceptor implements ReaderInterceptor, WriterInterceptor {
    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context)
            throws IOException, WebApplicationException {
        try {
            System.out.println("[aroundReadFrom] before");
            return context.proceed();
        } finally {
            System.out.println("[aroundReadFrom] after");
        }
    }

    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
            throws IOException, WebApplicationException {
        System.out.println("[aroundWriteTo] before");
        context.proceed();
        System.out.println("[aroundWriteTo] after");
    }
}
```

- エンティティの読み取りの前後に処理を入れたい場合は `ReaderInterceptor` を実装する
    - `aroundReadFrom` メソッドを実装する
    - `ReaderInterceptorContext` の `proceed` メソッドを呼ぶと後続処理が呼ばれるので、その前後で処理を実装する
- エンティティの書き出しの前後に処理を入れたい場合は `WriterInterceptor` を実装する
    - `aroundWriteTo` メソッドを実装する
    - `WriterInterceptorContext` の `proceed` メソッドを呼ぶと後続処理が呼ばれるので、その前後で処理を実装する

#### プロパティを受け取る

**実装**

```java:MyPropertiesInterceptor.java
package sandbox.jaxrs.provider.interceptor;

import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.ext.InterceptorContext;
import jakarta.ws.rs.ext.ReaderInterceptor;
import jakarta.ws.rs.ext.ReaderInterceptorContext;
import jakarta.ws.rs.ext.WriterInterceptor;
import jakarta.ws.rs.ext.WriterInterceptorContext;

import java.io.IOException;

public class MyPropertiesInterceptor implements ReaderInterceptor, WriterInterceptor {
    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context)
            throws IOException, WebApplicationException {
        printProperties("aroundReadFrom", context);
        return context.proceed();
    }

    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
            throws IOException, WebApplicationException {
        printProperties("aroundWriteTo", context);
        context.proceed();
    }

    private void printProperties(String method, InterceptorContext context) {
        System.out.println("[" + method + "]");
        for (String name : context.getPropertyNames()) {
            System.out.println(name + ": " + context.getProperty(name));
        }
    }
}
```

- `aroundReadFrom`, `aroundWriteTo` のそれぞれで、コンテキストからプロパティの値を取得して出力している

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import sandbox.jaxrs.provider.interceptor.MyPropertiesInterceptor;

public class JaxRsClientMain {

    public static void main(String[] args) {
        ClientBuilder.newClient()
                .property("foo", "FOO@Client")
                .register(MyPropertiesInterceptor.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .property("hoge", "HOGE@Invocation.Builder")
                .buildPost(Entity.text("Hello"))
                .property("fuga", "FUGA@Invocation")
                .invoke()
                .readEntity(String.class);
    }
}
```

- `Client` と `Invocation.Builder`, `Invocation` のそれぞれでプロパティを設定している

**実行結果**

```text:実行結果
[aroundWriteTo]
hoge: HOGE@Invocation.Builder
fuga: FUGA@Invocation
[aroundReadFrom]
hoge: HOGE@Invocation.Builder
fuga: FUGA@Invocation
```

- `Invocation.Builder` と `Invocation` で設定したプロパティのみが出力されている
- `Client` で設定したプロパティは出力されない

**説明**

```java:MyPropertiesInterceptor.java
    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context)
            throws IOException, WebApplicationException {
        printProperties("aroundReadFrom", context);
        return context.proceed();
    }

    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
            throws IOException, WebApplicationException {
        printProperties("aroundWriteTo", context);
        context.proceed();
    }

    private void printProperties(String method, InterceptorContext context) {
        System.out.println("[" + method + "]");
        for (String name : context.getPropertyNames()) {
            System.out.println(name + ": " + context.getProperty(name));
        }
    }
```

- `ReaderInterceptorContext` および `WriterInterceptorContext` からは、リクエストコンテキストに設定されているプロパティを参照できる
- リクエストコンテキストのプロパティは、 `Invocation.Builder` および `Invocation` の `property` で設定できる
- `Client` の `property` で設定したプロパティは、ここでは参照できない
    - `Client` の `property` で設定したプロパティは、 `Configuration` から参照できる
    - 詳しくは [フィーチャー](#フィーチャーfeatures) を参照

## フィーチャー(Features)

**実装**

```java:MyFeature.java
package sandbox.jaxrs.provider.feature;

import jakarta.ws.rs.core.Feature;
import jakarta.ws.rs.core.FeatureContext;
import sandbox.jaxrs.provider.MySimpleEntityProvider;
import sandbox.jaxrs.provider.filter.MyFilter;
import sandbox.jaxrs.provider.interceptor.MyInterceptor;

public class MyFeature implements Feature {
    @Override
    public boolean configure(FeatureContext context) {
        context.register(MyFilter.class)
               .register(MyInterceptor.class)
               .register(MySimpleEntityProvider.class);
        return true;
    }
}
```

- `Feature` インタフェースを実装したクラスを作成
- 自作のフィルタ、インターセプター、エンティティプロバイダをそれぞれ登録している
    - それぞれの実装は、ここまでのサンプルで使ってきた実装と同じ

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.feature.MyFeature;

public class JaxRsClientMain {

    public static void main(String[] args) {
        ClientBuilder.newClient()
                .register(MyFeature.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.text("Hello"), String.class);
    }
}
```

- `MyFeature` を `register` で登録している

**実行結果**

```text:実行結果(サーバー側)
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: text/plain
x-test-header: TestValue
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 5
===body===
Hello
==========
```

- フィルタで設定した `x-test-header` ヘッダーが渡っている

```text:実行結果(クライアント側)
[aroundWriteTo] before
writeTo
[aroundWriteTo] after
response length = 5
[aroundReadFrom] before
readFrom
[aroundReadFrom] after
```

- フィルタ、インターセプター、エンティティプロバイダの処理が走っている

**説明**

```java:MyFeature.java
public class MyFeature implements Feature {
    @Override
    public boolean configure(FeatureContext context) {
        context.register(MyFilter.class)
               .register(MyInterceptor.class)
               .register(MySimpleEntityProvider.class);
        return true;
    }
```

- フィーチャーは `Feature` インタフェースを実装することで作成できる
- `configure` メソッドを実装する
    - 引数で受け取る `FeatureContext` を使ってプロバイダの登録など、任意の設定ができる
    - このフィーチャーが機能を有効にした場合は `true` を返し、機能を有効にしない判断した場合は `false` を返す
- このように、フィーチャーを用いると複数のプロバイダの設定などを1つにまとめることができる
    - Jackson のエンティティプロバイダなどは、実際はこのフィーチャーで提供されている
    - ただ、設定の有効化はサービス・ローダーの仕組みを使って裏で勝手に行われているので、表にはあまり出てこない

### プロパティを受け取る

**実装**

```java:MyPropertiesFeature.java
package sandbox.jaxrs.provider.feature;

import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.Feature;
import jakarta.ws.rs.core.FeatureContext;

public class MyPropertiesFeature implements Feature {
    @Override
    public boolean configure(FeatureContext context) {
        Configuration configuration = context.getConfiguration();
        for (String name : configuration.getPropertyNames()) {
            System.out.println(name + ": " + configuration.getProperty(name));
        }
        return true;
    }
}
```

- `FeatureContext` から `Configuration` を取得し、プロパティを出力している

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.feature.MyPropertiesFeature;

public class JaxRsClientMain {

    public static void main(String[] args) {
        ClientBuilder.newBuilder()
                .property("hoge", "HOGE@ClientBuilder")
                .build()
                .register(MyPropertiesFeature.class)
                .property("fuga", "FUGA@Client")
                .target("http://localhost:8080/debug/print")
                .property("piyo", "PIYO@WebTarget")
                .request()
                .property("hoge", "HOGE@Invocation.Builder")
                .post(Entity.text("Hello"), String.class);
    }
}
```

- `ClientBuilder`, `Client`, `WebTarget` および `Invocation.Builder` のそれぞれでプロパティを設定している

**実行結果**

```text:実行結果(クライアント側)
hoge: HOGE@ClientBuilder
fuga: FUGA@Client
piyo: PIYO@WebTarget
```

- `ClientBuilder`, `Client`, `WebTarget` で設定したプロパティが出力されている
- `Invocation.Builder` で設定したプロパティは出力されていない

**説明**

```java:MyPropertiesFeature.java
    public boolean configure(FeatureContext context) {
        Configuration configuration = context.getConfiguration();
        for (String name : configuration.getPropertyNames()) {
            System.out.println(name + ": " + configuration.getProperty(name));
        }
        return true;
    }
```

- `FeatureContext` からは、 `Configuration` に設定されたプロパティを参照できる
- `Configuration` のプロパティには、 `Configurable` 経由で設定した値が反映されている
- 大きく `Configurable` と `Invocation.Builder` の二か所でプロパティを設定できるが、それぞれは参照できる箇所が分かれている
- 以下に整理する

```plantuml
scale 0.7

interface Configurable<T> {
  property(name: String, value: Object): T
  getConfiguration(): Configuration
}
interface Client extends Configurable
class ClientBuilder implements Configurable
interface WebTarget extends Configurable

interface FeatureContext extends Configurable
interface Feature {
  configure(context: FeatureContext): boolean
}
interface Configuration {
  getProperty(name: String): Object
}

Configurable .> Configuration: プロパティの反映
FeatureContext <. Feature
Configuration <.. Feature: プロパティの参照
```

- `Configurable` で設定したプロパティは `Configuration` に反映される
- `Configuration` に反映されたプロパティは `Feature` を実装したクラスから参照できる

```plantuml
interface "Invocation.Builder" as Builder {
  property(name: String, value: Object): Builder
}

interface InterceptorContext {
  getProperty(name: String): Object
}

interface ReaderInterceptorContext extends InterceptorContext
interface WriterInterceptorContext extends InterceptorContext

interface ReaderInterceptor {
  aroundReadFrom(context: ReaderInteceptorContext): Object
}

interface WriterInterceptor {
  aroundWriteTo(context: WriterInteceptorContext): Object
}

interface ClientRequestContext {
  getProperty(name: String): Object
}

interface ClientRequestFilter {
  filter(context: ClientRequestContext): void
}


interface ClientResponseFilter {
  filter(reqContext: ClientRequestContext, respContext: ClientResponseContext): void
}

interface Invocation {
  property(name: String, value: Object): Invocation
}

Builder ..> InterceptorContext: プロパティの反映
Builder ..> ClientRequestContext: プロパティの反映
Invocation ..> InterceptorContext: プロパティの反映
Invocation ..> ClientRequestContext: プロパティの反映

ReaderInterceptorContext <.. ReaderInterceptor: プロパティの参照
WriterInterceptorContext <.. WriterInterceptor: プロパティの参照
ClientRequestContext <.. ClientRequestFilter: プロパティの参照
ClientRequestContext <.. ClientResponseFilter: プロパティの参照
```

- `Invocation.Builder` および `Invocation` で設定したプロパティはリクエストコンテキストに保存される
- リクエストコンテキストのプロパティは、 `InterceptorContext` および `ClientRequestContext` から参照できる
- `InterceptorContext` はインターセプタから、 `ClientRequestContext` はフィルターから参照できる

# UriBuilder を用いた URI の構築方法

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("/path")
                            .path("foo")
                            .path("bar")
                            .scheme("https")
                            .port(9090)
                            .host("localhost")
                            .queryParam("hoge", "HOGE")
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
https://localhost:9090/path/foo/bar?hoge=HOGE
```

- `UriBuilder` を用いることで、柔軟に URI を構築することができる
- `UriBuilder` にはファクトリメソッドがいくつか用意されており、そこから構築をスタートする

## ファクトリメソッド
### 空のインスタンスを生成する

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.newInstance()
                            .path("/hello")
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
/hello
```

- `newInstance` は、何も URI の情報が設定されていない空の状態でインスタンスを生成する

### 既存の URI 文字列をベースに生成する

```java:UriBuilderMain.javapackage sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromUri("http://localhost:8080/test")
                            .path("foo")
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
http://localhost:8080/test/foo
```

- `fromUri` は、既存の URI 文字列をベースにしてビルダーを生成する


### 既存の URI オブジェクトをベースに生成する

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI baseUri = URI.create("http://localhost:8080/test");
        URI uri = UriBuilder.fromUri(baseUri)
                            .path("foo")
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
http://localhost:8080/test/foo
```

- `fromUri` は、既存の `URI` オブジェクトをベースにしてビルダーを生成する

### 既存の相対パスをベースに生成する

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("/test")
                            .path("foo")
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
/test/foo
```

- `fromPath` は、既存の相対パスをベースにしてビルダーを生成する


### 既存の Link オブジェクトをベースに生成する

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.Link;
import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        Link link = Link.fromUri("http://localhost:8080/hello").build();
        URI uri = UriBuilder.fromLink(link)
                            .path("foo")
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
http://localhost:8080/hello/foo
```

### リソースクラスをベースに生成する

```java:MyResource.java
package sandbox.jaxrs.resource;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("/my-resource")
public class MyResource {

    @GET
    @Path("hoge")
    public String hogeMethod() {
        return "HOGE";
    }

    @GET
    @Path("fuga")
    public String fugaMethod() {
        return "FUGA";
    }
}
```

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;
import sandbox.jaxrs.resource.MyResource;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromResource(MyResource.class)
                            .path("foo")
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
/my-resource/foo
```

- `fromResource` は、 JAX-RS のリソースクラスに設定された `@Path` の値をベースにしたビルダーを生成する
- 引数にはリソースクラスの `Class` オブジェクトを渡す

### リソースメソッドをベースに生成する

```java:MyResource.java
package sandbox.jaxrs.resource;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("/my-resource")
public class MyResource {

    @GET
    @Path("hoge")
    public String hogeMethod() {
        return "HOGE";
    }

    @GET
    @Path("fuga")
    public String fugaMethod() {
        return "FUGA";
    }
}
```


```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;
import sandbox.jaxrs.resource.MyResource;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromMethod(MyResource.class, "hogeMethod")
                            .path("foo")
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
hoge/foo
```

- `fromMethod` は、 JAX-RS のリソースクラスのメソッドに設定された `@Path` の値をベースにしたビルダーを生成する
- 引数にはリソースクラスの `Class` オブジェクトとメソッドの名前を渡す
- 指定された名前で `@Path` のつけられたメソッドが１つしか存在しないことが前提
    - 複数存在する場合はエラーになる


## スキーム・ホスト・ポートを設定する

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("test")
                            .scheme("https")
                            .host("localhost")
                            .port(8080)
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
https://localhost:8080/test
```

- `scheme` でスキーム
  `host` でホスト
  `port` でポートを設定できる

## パスを追加する
```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("test")
                            .path("foo")
                            .path("/bar")
                            .path("fizz/fizz buzz")
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
test/foo/bar/fizz/fizz%20buzz
```

- `path` でパスを追加できる
- パスとパスの間のスラッシュ (`/`) の要・不要は自動的に判断される
- スラッシュ (`/`) はそのまま追加される
- スラッシュ以外の URL エンコードが必要な文字はエンコードされる

### リソースクラスのパスを追加する
```java:MyResource.java
package sandbox.jaxrs.resource;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("/my-resource")
public class MyResource {

    @GET
    @Path("hoge")
    public String hogeMethod() {
        return "HOGE";
    }

    @GET
    @Path("fuga")
    public String fugaMethod() {
        return "FUGA";
    }
}
```

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;
import sandbox.jaxrs.resource.MyResource;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("test")
                            .path(MyResource.class)
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
test/my-resource
```

- `path(Class)` を使うと、指定したリソースクラスの `@Path` に設定されたパスを追加できる

### リソースクラスのメソッドのパスを追加する

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;
import sandbox.jaxrs.resource.MyResource;

import java.lang.reflect.Method;
import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) throws Exception {
        Method fugaMethod = MyResource.class.getMethod("fugaMethod");

        URI uri = UriBuilder.fromPath("test")
                            .path(MyResource.class, "hogeMethod")
                            .path(fugaMethod)
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
test/hoge/fuga
```

- `path(Class, String)` および `path(Method)` を使うと、指定したリソースクラスのメソッドに設定された `@Path` の値を追加できる

### パスを置き換える

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromUri("http://localhost/foo/bar")
                            .replacePath("fizz/buzz")
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
http://localhost/fizz/buzz
```

- `replacePath` を使うと、 URI のパスの部分を置き換えることができる


## クエリパラメータを設定する

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("test")
                            .queryParam("hoge", "HOGE")
                            .queryParam("fuga", "FUGA", "FUGA", "FUGA")
                            .queryParam("piyo", "PIYO/PIYO%2FPIYO")
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
test?hoge=HOGE&fuga=FUGA&fuga=FUGA&fuga=FUGA&piyo=PIYO%2FPIYO%2FPIYO
```

- `queryParam` メソッドでクエリパラメータを追加できる
    - 値は可変長引数で複数指定可能
- URLエンコードが必要な部分は自動的にエンコードされる
    - 既にエンコード済みの部分はそのまま埋め込まれる

### クエリパラメータを文字列で直接設定する

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromUri("http://localhost/test?foo=FOO")
                            .replaceQuery("hoge=HOGE&fuga=FUGA FUGA&piyo=PIYO%20PIYO")
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
http://localhost/test?hoge=HOGE&fuga=FUGA%20FUGA&piyo=PIYO%20PIYO
```

- `replaceQuery(String)` を使うと、クエリパラメータを指定した文字列で置き換えることができる
- URL エンコードが必要な部分は自動的にエンコードされる
    - 既にエンコード済みの部分はそのまま埋め込まれる

## フラグメントを設定する

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("test")
                            .fragment("hoge")
                            .build();

        System.out.println(uri);
    }
}
```

```text:実行結果
test#hoge
```

- `fragment` でフラグメントを設定できる

## テンプレート

```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromUri("http://localhost/{foo}/{bar}/{foo}")
                            .build("FOO", "BAR");

        System.out.println(uri);
    }
}
```

```text:実行結果
http://localhost/FOO/BAR/FOO
```

- `{名前}` のように埋め込みパラメータを宣言することで、 URI をテンプレート化できる
- `build` でパラメータに埋め込む値を指定する場合、パラメータが出現した順序で埋め込みが行われる

### Map でパラメータを指定する
```java:UriBuilderMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;
import java.util.Map;

public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromUri("http://localhost/{foo}/{bar}/{foo}")
                            .buildFromMap(Map.of("foo", "FOO", "bar", "BAR"));

        System.out.println(uri);
    }
}
```

```text:実行結果
http://localhost/FOO/BAR/FOO
```

- `buildFromMap` を使えば `Map` でパラメータを埋め込むことも可能
- その他、細かい使い方は `Client` 構築時のテンプレートと同じなので [そちら](#テンプレートを使用する) を参照

# コネクタの変更

- Jersey では、実際に HTTP 通信を行う部分の実装にデフォルトでは `HttpUrlConnection` を利用している
- この実際に HTTP 通信を行うコンポーネントのことを**コネクタ**と呼ぶ
- コネクタには、例えば Apache HTTP Client を使うものや java.net.http の `HttpClient` を使うものが用意されている

## HttpUrlConnection の制限
- デフォルトで利用される `HttpUrlConnection` でも多くのケースでは問題なく利用できる
- ただし、 `HttpUrlConnection` には以下に挙げるような制限がある
    - PATCH メソッドが利用できない
        - `HttpUrlConnection` は HTTP 1.1 を実装しているため、 PATCH メソッドが利用できない
    - デフォルトでは利用できないヘッダーがある
        - `HttpUrlConnection` では、以下のリクエストヘッダーはデフォルトでは設定できないように制限されている
            - `Access-Control-Request-Headers`
            - `Access-Control-Request-Method`
            - `Connection`
                - ただし、値が `Closed` の場合は許可される
            - `Content-Length`
            - `Content-Transfer-Encoding-`
            - `Host`
            - `Keep-Alive`
            - `Origin`
            - `Trailer`
            - `Transfer-Encoding`
            - `Upgrade`
            - `Via`
            - `Sec-` で始まるすべてのヘッダー
- 一応、それぞれ `HttpUrlConnection` のまま回避する方法が用意されているが、それよりは Apache HTTP Client のコネクタなどの他のコネクタに切り替えることが推奨されている

## コネクタを変更する
PATCHメソッドのリクエストを投げる場合を例にして、コネクタを切り替える方法を説明する。

### デフォルトコネクタの場合

**実装**

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;

public class JaxRsClientMain {

    public static void main(String[] args) 
        ClientBuilder.newClient()
                .target("http://localhost:8080/debug/print")
                .request()
                .method("PATCH", Entity.text("Hello"))
                .close();
    }
}
```

**実行結果**

```text:実行結果(クライアント側)
Exception in thread "main" jakarta.ws.rs.ProcessingException: java.net.ProtocolException: Invalid HTTP method: PATCH
	at org.glassfish.jersey.client.internal.HttpUrlConnector.apply(HttpUrlConnector.java:288)
	...
Caused by: java.net.ProtocolException: Invalid HTTP method: PATCH
	at java.base/java.net.HttpURLConnection.setRequestMethod(HttpURLConnection.java:491)
	...
	... 12 more
```

- デフォルトのままだと、 PATCH メソッドのリクエストは送信できない

### Java java.net.http client の場合

**実装**

```gradle:build.gradle
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓以下を追加
    implementation "org.glassfish.jersey.connectors:jersey-jnh-connector:3.1.8"
}
```

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.jnh.connector.JavaNetHttpConnectorProvider;

public class JaxRsClientMain {

    public static void main(String[] args) {
        ClientConfig config = new ClientConfig();
        config.connectorProvider(new JavaNetHttpConnectorProvider());

        ClientBuilder.newClient(config)
                .target("http://localhost:8080/debug/print")
                .request()
                .method("PATCH", Entity.text("Hello"))
                .close();
    }
}
```

**実行結果**

```text:実行結果(サーバー側)
method=PATCH
requestURI=/debug/print
queryString=null
===headers===
content-length: 5
host: localhost:8080
content-type: text/plain
user-agent: Jersey/3.1.8 (Java HttpClient Connector 3.1.8)
===body===
Hello
==========
```

- PATCH メソッドでリクエストが送信できている

**説明**

```gradle:build.gradle
dependencies {
    ...
    implementation "org.glassfish.jersey.connectors:jersey-jnh-connector:3.1.8"
}
```

- コネクタに Java java.net.http client を使用する場合は、 [org.glassfish.jersey.connectors:jersey-jnh-connector](https://mvnrepository.com/artifact/org.glassfish.jersey.connectors/jersey-jnh-connector) を依存関係に追加する

```java:JaxRsClientMain.java
        ClientConfig config = new ClientConfig();
        config.connectorProvider(new JavaNetHttpConnectorProvider());

        ClientBuilder.newClient(config)
```

- 各コネクタは `ConnectorProvider` インタフェースを実装したクラスを提供している
    - Java java.net.http client の場合は `JavaNetHttpConnectorProvider`
- この `ConnectorProvider` のインスタンスを `ClientConfig` の `connectorProvider` メソッドで設定し、 `ClientConfig` を使って `Client` に生成することでコネクタを変更できる
- 他のコネクタの `ConnectorProvider` や Maven の依存関係については下記公式サイトの表を参照
    - [Table 5.1. List of Jersey Connectors | 5.5. Client Transport Connectors](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest31x/client.html#d0e5088)

# プロキシの設定方法

- Jersey でのプロキシの設定は、プロパティで行う
- キーは [ClientProperties](https://eclipse-ee4j.github.io/jersey.github.io/apidocs/3.1.8/jersey/org/glassfish/jersey/client/ClientProperties.html) に定義されている
    - `PROXY_URI`
    - `PROXY_USERNAME`
    - `PROXY_PASSWORD`
- なお、 `PROXY_URI` をサポートしているのは以下のコネクタのみ
    - `ApacheConnectorProvider`
    - `Apache5ConnectorProvider`
    - `GrizzlyConnectorProvider`
    - `HelidonConnectorProvider`
    - `NettyConnectorProvider`
    - `Jetty11ConnectorProvider`
    - `JettyConnectorProvider`
- また、 `PROXY_USERNAME` と `PROXY_PASSWORD` をサポートしているのは以下のコネクタのみ
    - `ApacheConnectorProvider`
    - `JettyConnectorProvider`
- プロパティに関する情報は [A.5. Client configuration properties](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest31x/appendix-properties.html#d0e20628) を参照

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import org.glassfish.jersey.apache.connector.ApacheConnectorProvider;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;

public class JaxRsClientMain {

    public static void main(String[] args) {
        ClientConfig config = new ClientConfig();
        config.connectorProvider(new ApacheConnectorProvider())
              .property(ClientProperties.PROXY_URI, "http://proxy.host.name:8080")
              .property(ClientProperties.PROXY_USERNAME, "username")
              .property(ClientProperties.PROXY_PASSWORD, "password");

        ClientBuilder.newClient(config)
                .target("http://localhost:8080/debug/print")
                .request()
                .method("PATCH", Entity.text("Hello"))
                .close();
    }
}
```

# 非同期実行

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;

import java.util.concurrent.Future;

public class JaxRsClientMain {

    public static void main(String[] args) throws Exception {
        try (Client client = ClientBuilder.newClient()) {
            Future<String> future = client
                    .target("http://localhost:8080/debug/print")
                    .request()
                    .async()
                    .post(Entity.text("Hello"), String.class);

            String response = future.get();
            System.out.println(response);
        }
    }
}
```

- `Invocation.Builder` の `async` メソッドを使用すると、リクエストの実行を非同期（別スレッド）で行う `AsyncInvoker` を作成できる
- `AsyncInvoker` には `get` や `post` などの HTTP メソッドに対応する実行メソッドがある
    - 任意のメソッドを指定する場合は `method` メソッドを使用する
- 実行メソッドの戻り値は `Future` となっており、 `get` メソッドで非同期処理の結果を待つことができる

## Invocation を非同期で実行する

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;

import java.util.concurrent.Future;

public class JaxRsClientMain {

    public static void main(String[] args) throws Exception {
        try (Client client = ClientBuilder.newClient()) {
            Invocation invocation = client
                    .target("http://localhost:8080/debug/print")
                    .request()
                    .buildPost(Entity.text("Hello"));

            Future<String> future = invocation.submit(String.class);

            String response = future.get();
            System.out.println(response);
        }
    }
}
```

- `Invocation` の `submit` メソッドを使うと、リクエストを非同期で実行できる
- `submit` の戻り値は `Future` になっているので、非同期の処理結果を待機できる

## ExecutorService を指定する

```java:JaxRsClientMain.java
package sandbox.jaxrs.client;

import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class JaxRsClientMain {

    public static void main(String[] args) throws Exception {
        try (ExecutorService executorService = Executors.newSingleThreadExecutor()) {
            ClientBuilder builder = ClientBuilder.newBuilder()
                                                 .executorService(executorService);

            try (Client client = builder.build()) {
                Invocation invocation = client
                        .target("http://localhost:8080/debug/print")
                        .request()
                        .buildPost(Entity.text("Hello"));

                Future<String> future = invocation.submit(String.class);

                String response = future.get();
                System.out.println(response);
            }
        }
    }
}
```

- `ClientBuilder` の `executorService` メソッドに `ExecutorService` を渡すことができる
- これにより非同期でリクエストを実行するときは、ここで設定された `ExecutorService` が作成したスレッドが使われるようになる

# 参考
- [5. Client API | Jakarta RESTful Web Services](https://jakarta.ee/specifications/restful-ws/3.1/jakarta-restful-ws-spec-3.1#client_api)
- [Chapter 5. Client API | Eclipse Jersey](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest31x/client.html)
- [Jersey 3.1.8 User Guide](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest31x/index.html)
- [Javadoc (Jakarta RESTful WS API 3.1.0 API)](https://jakarta.ee/specifications/restful-ws/3.1/apidocs/)
- [UriBuilder (Jakarta RESTful WS API 3.1.0 API)](https://jakarta.ee/specifications/restful-ws/3.1/apidocs/jakarta.ws.rs/jakarta/ws/rs/core/uribuilder)