Volleyでmultipartリクエストを送信する


# 前段

HTTP POSTでファイルを送信するにはmultipartリクエストを送信する必要があるのですが、Volleyにそれ用のリクエストクラスが無かったので嫌々自作しました。本サンプルではStringRequestを拡張して作成していますが、適宜変更してください。

# ソース

```java:MultipartStringRequest.java
public class MultipartStringRequest extends StringRequest {

    private static final String CRLF = "\r\n";

    private final String boundary = "----boundary" + System.currentTimeMillis();

    // テキストパラメータ nameとvalue
    private Map<String, String> textParams = new HashMap<>();
    // バイナリパラメータ nameとファイルパス
    private Map<String, String> binaryParams = new HashMap<>();

    public MultipartStringRequest(String url, Response.Listener<String> listener, Response.ErrorListener errorListener) {
        super(Method.POST, url, listener, errorListener);
    }

    @Override
    public String getBodyContentType() {
        return "multipart/form-data;boundary=" + boundary;
    }

    @Override
    public byte[] getBody() throws AuthFailureError {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);
        try {
            writeTextPart(dos);
            writeBinaryPart(dos);
            dos.writeBytes("--" + boundary + "--" + CRLF);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bos.toByteArray();
    }

    private void writeTextPart(DataOutputStream dos) throws IOException {
        for (Map.Entry<String, String> e: textParams.entrySet()) {
            dos.write(encodeToByteArray("Content-Disposition: form-data; name=\"" + e.getKey() + "\"" + CRLF));
            dos.writeBytes(CRLF);
            dos.write(encodeToByteArray(e.getValue() + CRLF));
            dos.writeBytes("--" + boundary + CRLF);
        }
    }

    private void writeBinaryPart(DataOutputStream dos) throws IOException {
        for (Map.Entry<String, String> e: binaryParams.entrySet()) {
            UploadFile uploadFile = new UploadFile(e.getValue());
            dos.write(encodeToByteArray("Content-Disposition: form-data; name=\"" + e.getKey() + "\"; filename=\"" + uploadFile.getName() + "\"" + CRLF));
            dos.writeBytes("Content-Type: " + uploadFile.getMimeType() + CRLF);
            dos.writeBytes(CRLF);
            dos.write(uploadFile.getByteArray()); dos.writeBytes(CRLF);
            dos.writeBytes(CRLF);
            dos.writeBytes("--" + boundary + CRLF);
        }
    }

    public void setTextParams(Map<String, String> textParams) {
        this.textParams = textParams;
    }

    public void setBinaryParams(Map<String, String> binaryParams) {
        this.binaryParams = binaryParams;
    }

    protected byte[] encodeToByteArray(String str) {
        return str.getBytes();
    }
}
```

```java:UploadFile.java
// 上記MultipartStringRequestと同じパッケージに

class UploadFile {

    private final String filePath;

    UploadFile(String filePath) {
        this.filePath = filePath;
    }

    String getName() {
        return PathUtil.getFileName(filePath);
    }

    String getMimeType() {
        MimeTypeMap mime = MimeTypeMap.getSingleton();
        String extension = PathUtil.getExtension(filePath);
        return extension != null ? mime.getMimeTypeFromExtension(extension) : null;
    }

    byte[] getByteArray() throws IOException {
        return FileUtil.readAsByteArray(new File(filePath));
    }
}
```

```java:FileUtil.java
public class FileUtil {

    public static byte[] readAsByteArray(File file) throws IOException {
        long length = file.length();
        byte[] bytes = new byte[(int)length];

        InputStream is = new FileInputStream(file);
        int offset = 0;
        int numRead;
        while ((offset < bytes.length) && ((numRead = is.read(bytes, offset, bytes.length - offset)) >= 0)) {
            offset += numRead;
        }
        is.close();
        return bytes;
    }
}
```

```java:PathUtil.java
public class PathUtil {

    public static String getFileName(String path) {
        return new File(path).getName();
    }

    public static String getExtension(String path) {
        String extension = null;
        int index = path.lastIndexOf(".");
        if (index > 0) {
            extension = path.substring(index + 1);
        }
        return extension;
    }
}
```

使い方は次のような感じです。普通のテキストパラメータはnameとvalueのハッシュを、画像等のアップロードファイルはnameとファイルパスのハッシュをセットしてあげます。

```java:クライアント.java
MultipartStringRequest request = new MultipartStringRequest(
        "https://example.com/api/entryPoint",
        new Response.Listener<String>() {
            @Override
            public void onResponse(String response) {
                // do something
            }
        },
        new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                // do something
            }
        }
);
Map<String, String> textParams = new HashMap<>();
textParams.put("username", "hoge");
textParams.put("email", "a@b.com");
request.setTextParams(textParams);
Map<String, String> binaryParams = new HashMap<>();
binaryParams.put("uploadFile", "/path/to/file.jpeg");
request.setBinaryParams(binaryParams);
queue.add(request);
```

# おまけ

multipartリクエストで、さらに「Shift-JIS」で「任意のRefererをセットする」という仕様のリクエストの作成例です。先ほどの`MultipartStringRequest`を継承して作成しています。

```java:HogeRequest.javajava
public class HogeRequest extends MultipartStringRequest {

    private final Map<String, String> headers = new HashMap<>();

    public HogeRequest(String url, Response.Listener<String> listener, Response.ErrorListener errorListener) {
        super(url, listener, errorListener);
    }

    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        return headers;
    }

    @Override
    protected String getParamsEncoding() {
        return "Shift_JIS";
    }

    public void setReferer(String url) {
        headers.put("Referer", url);
    }

    @Override
    protected byte[] encodeToByteArray(String str) {
        byte[] result = null;
        try {
            result = str.getBytes("SJIS");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return result;
    }
}
```