読者です 読者をやめる 読者になる 読者になる

リア充爆発日記

You don't even know what ria-ju really is.

HttpURLConnectionでmultipart/form-dataをPOSTする

たいがいのアプリで必要であろうHTTP通信。

シンプルなRESTだけあればいいのに、既存のライブラリはどれもデカく、アプリサイズが大きくなるのが嫌だったので自前実装を試みた。
GB以降だったらHttpURLConnectionがいいってどこかのエライ人が言っていたのでHttpURLConnectionでやることにした。

シンプルにPOSTを発行するところまではすぐできたんだけど

  • マルチバイト文字の送信
  • ファイルの送信
  • を同時に

を達成するまでが、すげーハマった。。が、とにかく動いた。

なんだかどんどんわかんなくなってたくさん見直したいところがあるウンコードだし、機能的にも201以外は例外投げるとか雑な状態ではあるものの、サンプルがほとんど見つからなかったので誰かの役に立つかもしれないので晒しておきますゆえ。。。

public class HttpClientForPost extends HttpClient {
    private static final String TAG = HttpClientForPost.class.getSimpleName();
    public static final String HOST = "http://api.example.com:3000"; 
    public static final String CRLF = "\r\n";
    public static final String BOUNDARY = "---*#asdkfjaewaefa#";
    public static final String TWO_HYPHEN = "--";

    private Map<String, String> textDataMap = new HashMap<String, String>();
    private ArrayList<MultiPartFile> multiPartFileArrayList = new ArrayList<MultiPartFile>();
    private StringBuilder dummyBody = new StringBuilder();

    protected HttpClientForPost(URL url) throws IOException {
        conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setDoOutput(true);
        conn.setReadTimeout(10 * 1000);
        conn.setConnectTimeout(10 * 1000);
        conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + HttpClientForPost.BOUNDARY);
    }

    public void addTextValue(String key, String value) {
        textDataMap.put(key, value);
    }

    public Map<String, String> getTextDataMap() {
        return textDataMap;
    }

    public void addFile(MultiPartFile multiPartFile){
        multiPartFileArrayList.add(multiPartFile);
    }

    public ArrayList<MultiPartFile> getMultiPartFileArrayList() {
        return multiPartFileArrayList;
    }

    private String createTextBody() throws UnsupportedEncodingException {
        StringBuilder body = new StringBuilder();
        body.append(CRLF);

        for (Map.Entry<String, String> entry : textDataMap.entrySet()) {
            body
                .append(TWO_HYPHEN).append(BOUNDARY).append(CRLF)
                .append("Content-Disposition: form-data; name=\"").append(entry.getKey()).append("\"").append(CRLF)
                .append(CRLF)
                .append(entry.getValue()).append(CRLF);
        }

        return body.toString();
    }

    private int write(BufferedOutputStream out, String string) throws IOException {
        dummyBody.append(string);
        out.write(string.getBytes("UTF-8"));
        return string.length();
    }

    private int write(BufferedOutputStream out, byte[] bytes) throws IOException {
        dummyBody.append("#");
        out.write(bytes);
        return bytes.length;
    }

    public HttpResponse post() throws IOException, NoParameterException, HttpPostException {
        BufferedOutputStream out =  new BufferedOutputStream(conn.getOutputStream());
        int contentLength = 0;
        contentLength += write(out, CRLF);

        if ( !parameterExists() ) {
            throw new NoParameterException();
        }

        if (textDataMap.size() > 0) {
            contentLength += write(out, createTextBody());
        }

        out.flush();

        for (MultiPartFile multiPartFile : multiPartFileArrayList) {
            Log.d(TAG, "size:" + multiPartFileArrayList.size());
            contentLength += write(out, (TWO_HYPHEN + BOUNDARY + CRLF));
            String header = "Content-Disposition: form-data; name=\"" + multiPartFile.getName() + "\"; filename=\"" + multiPartFile.getFileName() + "\"" + CRLF +
                "Content-Type: " + multiPartFile.getMimeType() + CRLF;


            contentLength += write(out, header);
            contentLength += write(out, CRLF);

            InputStream inputStream = multiPartFile.getInputStream();
            byte[] input = new byte[inputStream.available()];
            while(inputStream.read(input) > -1) {
                contentLength += input.length;
                write(out, input);
            }

            contentLength += write(out, CRLF);

        }

        contentLength += write(out, (TWO_HYPHEN + BOUNDARY + TWO_HYPHEN + CRLF));

        Log.d(TAG, dummyBody.toString());

        out.flush();
        out.close();

        int responseCode = conn.getResponseCode();
        String responseMessage = conn.getResponseMessage();
        if (responseCode != 201) {
            String errorMessage = "responseCode: " + responseCode + "\nresponseMessage: " + responseMessage;
            Log.e(TAG, errorMessage);
            throw new HttpPostException(errorMessage);
        }
        InputStream in = new BufferedInputStream(conn.getInputStream());
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        StringBuilder response = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            response.append(line).append("\n");
        }

        return new HttpResponse(responseCode, responseMessage, response.toString());
    }

    public void disconnect() {
        conn.disconnect();
    }

    public URL getURL() {
        return conn.getURL();
    }

    private boolean parameterExists() {
        return multiPartFileArrayList.size() > 0 || textDataMap.size() > 0;
    }

    public static String wrapParameterName(String wrap, String param) {
        return wrap + "[" + param + "]";
    }


}

public class HttpResponse {
    private static final String TAG = HttpResponse.class.getSimpleName();

    private int responseCode;
    private String responseMessage;
    private String responseBody;

    public HttpResponse(int responseCode, String responseMessage, String responseBody) {
        this.responseCode = responseCode;
        this.responseMessage = responseMessage;
        this.responseBody = responseBody;
    }

    public int getResponseCode() {
        return responseCode;
    }

    public String getResponseMessage() {
        return responseMessage;
    }

    public String getResponseBody() {
        return responseBody;
    }
}

public class MultiPartFile {
    private static final String TAG = MultiPartFile.class.getSimpleName();
    private String name;
    private String fileName;
    private InputStream inputStream;

    public MultiPartFile(String name, String fileName, InputStream is) {
        this.name = name;
        this.fileName = fileName;
        this.inputStream = is;
    }

    public String getMimeType() {
        ExtensionMimeDetector detector = new ExtensionMimeDetector();
        Object[] mimeTypes = detector.getMimeTypes(fileName).toArray();
        MimeType mimeType = (MimeType)mimeTypes[0];

        return mimeType.toString();
    }

    public String getName() {
        return name;
    }

    public String getFileName() {
        return fileName;
    }

    public InputStream getInputStream() {
        return inputStream;
    }
}

ざっくりした処理の流れとしては、送信したいテキストとかファイルをaddして、最後にsend()し、send()はテキストデータを先にbodyにまとめ、その後にファイルを追加していく、という感じ。

せっかくハマったのでどこでどうハマってどう解決したか書こうと思ったけど、翌日になって確認してみると、ポイントだと思っていたコードを外してもちゃんと動いたりして、どこがどうだったのかよくわからなくなったので、それは書けない。夜遅くまでコード書くのはやめようね。