Feignでファイルダウンロードを実現する方法

## 1. はじめに

`Feign`のデフォルトの機能ではファイルダウンロードやアップロードといったバイナリを扱うHTTPリクエストの操作には対応していません。
今回は`Feign`でファイルダウンロードを実現する方法について説明したいと思います。
`Feign`の基本的な使い方については以下の記事で説明しているのでそちらを参照してください。

* [インターフェースだけでAPIクライアントを実装するFeignがすごく便利！](https://qiita.com/5zm/items/f7a48e80ad5ba4316748)

## 2. モデルクラスの定義

`feign.Response`から取得可能なヘッダ、ステータス、レスポンスリーズンのデータを保持するクラスです。
HTTPレスポンスのBODYに１つのファイルのバイナリデータが格納されている前提とし、ファイルを１つだけ保持する仕様としました。
あまり一般的ではありませんが、マルチパートのHTTPレスポンスを扱いたい場合は適宜修正してください。

```java:FileEntity.java
package com.example.feign.demo.download;

import java.io.File;
import java.io.Serializable;
import java.util.Collection;
import java.util.Map;

public class FileEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    private int status;
    private Map<String, Collection<String>> headers;
    private String reason;
    private File file;

    public FileEntity() {

    }

    public FileEntity(int status, Map<String, Collection<String>> headers,
            String reason, File file) {
        super();
        this.status = status;
        this.headers = headers;
        this.reason = reason;
        this.file = file;
    }

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("FileEntity [status=");
        builder.append(status);
        builder.append(", headers=");
        builder.append(headers);
        builder.append(", reason=");
        builder.append(reason);
        builder.append(", file=");
        builder.append(file);
        builder.append("]");
        return builder.toString();
    }

    // setter, getter omitted
}
```

## 3. APIのインターフェースの定義

APIは指定されたパスのファイルをダウンロードするというシンプルなものです。
戻り値は先ほど定義したモデルクラスの`FileEntity`です。
`java.io.File`や`java.nio.file.Path`を戻り値にしてしまうと、ファイルデータ以外の情報（ヘッダ、ステータスコード等）が取得できないため`FileEntity`を用意しました。

```java:DownloadApi.java
package com.example.feign.demo.download;

import feign.Param;
import feign.RequestLine;

public interface DownloadApi {

    @RequestLine("GET {path}")
    FileEntity download(@Param("path") String path);
}
```

## 4. ファイルダウンロードのDecoderを定義

今回の記事のポイントになります。HTTPレスポンスのデータ変換処理（デシリアライズ）は`Decoder`が担当します。
`feign.codec.Decoder`インターフェースを`implements`したファイルダウンロード用の`Decoder`を実装します。
目的の処理を`decode`メソッドに実装します。このメソッドの戻り値がAPIインターフェースの戻り値になります。
なお、今回は１ファイルの前提のためレスポンスのBODYのデータをそのままファイルに保存していますが、マルチパートのHTTPレスポンスの場合はパースして複数ファイルとして保存する必要があるので注意してください。

```java:FileEntityDecoder.java
package com.example.feign.demo.download;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import feign.FeignException;
import feign.Response;
import feign.Util;
import feign.codec.DecodeException;
import feign.codec.Decoder;

public class FileEntityDecoder implements Decoder {

    private static final int BUFFER_SIZE = 1 * 1024 * 1024;

    private String filePath;

    public FileEntityDecoder() {

    }

    public FileEntityDecoder(String filePath) {
        this.filePath = filePath;
    }

    /**
     * @see feign.codec.Decoder#decode(feign.Response, java.lang.reflect.Type)
     */
    @Override
    public Object decode(Response response,
            Type type) throws IOException, DecodeException, FeignException {

        if (response.status() == 404) {
            return Util.emptyValueOf(type);
        }
        if (response.body() == null) {
            return null;
        }

        return createFileEntity(response, filePath);
    }

    /**
     * Save download file on instructed filePath.
     * @param response feign's response
     * @param filePath download file path
     * @return FileEntity instance
     * @throws IOException
     */
    private FileEntity createFileEntity(Response response,
            String filePath) throws IOException {

        // 1. create File object on instructed file path or temporary
        File downloadFile = null;
        if (filePath == null) {
            downloadFile = File.createTempFile("download", null);
        } else {
            downloadFile = new File(filePath);
        }

        // 2. copy contents with buffering
        try (InputStream input = new BufferedInputStream(
                response.body().asInputStream());
                OutputStream out = new BufferedOutputStream(
                        new FileOutputStream(downloadFile));) {
            byte[] buffer = new byte[BUFFER_SIZE];
            long total = 0;
            int len = 0;
            while ((len = input.read(buffer)) != -1) {
                out.write(buffer, 0, len);
                out.flush();
                total = total + len;
                // System.out.println("writed : " + total);
            }
        }

        // 3. create FileEntity instance
        return new FileEntity(response.status(), response.headers(),
                response.reason(), downloadFile);
    }
}
```

## 5. お試し実行

APIインターフェースのインスタンスを生成する際、`decoder`メソッドの引数に今回実装した`FileEntityDecoder`のインスタンスを指定します。
認証が必要な`Proxy`を経由して、インターネットからPDFファイルをダウンロードするサンプルとしました。
`Proxy`対応のポイントとなる`createOkHttpClientCorrespondedProxy`メソッドの実装については[前回の記事](https://qiita.com/5zm/items/e9a78d73836d8692fae7)を参照してください。
エンドポイントのURLである`http://www.example.com`とパスの`/document/2018/pdf/sample.pdf`は各自で置き換えてください。

```java:DownloadDemo.java
package com.example.feign.demo.download;

import feign.Feign;
import feign.Logger;
import feign.slf4j.Slf4jLogger;
import okhttp3.OkHttpClient;

public class DownloadDemo {

    public static void main(String[] args) {
        // create instance of okhttp3.OkHttpClient corresponded proxy
        OkHttpClient client = createOkHttpClientCorrespondedProxy("yourProxyHost",
                8080, "proxyUserId", "proxyPass");

        // feign use proxy with authentication
        DownloadApi downloadApi = Feign.builder()
                // set instance of feign.Client.OkHttpClient
                .client(new feign.okhttp.OkHttpClient(client))
                .decoder(new FileEntityDecoder()) // use FileEntityDecoder
                .logger(new Slf4jLogger())        // use Slf4j
                .logLevel(Logger.Level.FULL)      // setting log level to most detail
                .target(DownloadApi.class, "http://www.example.com");

        // call api [GET /documents/2018/pdf/sample.pdf]
        FileEntity fileEntity = downloadApi.download(
                "/document/2018/pdf/sample.pdf");
        System.out.println(fileEntity);
    }
    
    // omitted createOkHttpClientCorrespondedProxy method
    // see Corresponded Proxy
}
```

```console:
22:32:58.703 [main] DEBUG feign.Logger - [DownloadApi#download] ---> GET http://www.example.com/documents/2018/pdf/sample.pdf HTTP/1.1
22:32:58.705 [main] DEBUG feign.Logger - [DownloadApi#download] ---> END HTTP (0-byte body)
22:32:58.819 [main] DEBUG feign.Logger - [DownloadApi#download] <--- HTTP/1.1 200 OK (113ms)
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] accept-ranges: bytes
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] content-length: 1656363
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] content-type: application/pdf
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] date: Thu, 05 Apr 2018 13:32:56 GMT
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] proxy-connection: Keep-Alive
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] server: Apache
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] set-cookie: server-20480-%3Fmntdlb01%3Fsfarm-web_ap=ECBBGDAKFAAA; Path=/
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] 
22:32:59.678 [main] DEBUG feign.Logger - [DownloadApi#download] Binary data
22:32:59.678 [main] DEBUG feign.Logger - [DownloadApi#download] <--- END HTTP (1656363-byte body)
FileEntity [status=200, headers={accept-ranges=[bytes], content-length=[1656363], content-type=[application/pdf], date=[Thu, 05 Apr 2018 13:32:56 GMT], proxy-connection=[Keep-Alive], server=[Apache], set-cookie=[server-20480-%3Fmntdlb01%3Fsfarm-web_ap=ECBBGDAKFAAA; Path=/]}, reason=OK, file=C:\Users\feign\AppData\Local\Temp\download4121273495791641378.tmp]
```

## 6. さいごに

今回は`Feign`でファイルダウンロードを実現するため、独自の`Decoder`を利用する方法について説明しました。
次回は`Feign`でファイルアップロードを実現する方法について説明したいと思います。