アプレットからWebサーバへファイルアップロード

アプレットからのファイルアップロード。これはなかなか大変だった。

具体的には、httpでmultipartのPOSTをしたい。複数のファイルや、一緒にテキストデータなんかも一緒にPOSTしたい。

Jakarta Commons HttpClientをアプレットで使うのは難しい

Jakarta Commons HttpClientというコンポーネントがあって、これを使うとhttp通信が手軽に使えるらしい。

これを使って実際に作ってみたけど、ダメだった。

  • 自分の作ったjarだけじゃなく、このHttpClientのjarにも署名が必要
  • 一緒に使う必要があるJakarta Commons Loggingのバグにより、権限エラーになる
  • 日本語ファイル名の文字化け

どれも回避しようと思えばできるんだけど、後でのメンテも含めて面倒になるため、あきらめた。

自分で通信部分を実装

Javaの標準機能だけを使って自分で通信部分を実装した。

String boundary = generateBoundary();

// 接続
URL url = new URL("http://example.com/upload.do"); // 送信先
URLConnection conn = url.openConnection();
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
DataOutputStream out = new DataOutputStream(conn.getOutputStream());

// テキストフィールド送信
out.writeBytes("--" + boundary + "\r\n");
out.writeBytes("Content-Disposition: form-data; name=\"text\"\r\n");
out.writeBytes("Content-Type: text/plain; charset=Shift_JIS\r\n\r\n");
out.write("テキスト".getBytes(this.charset));
out.writeBytes("\r\n");

// ファイルフィールド送信
File file = new File("c:\\files\\file.zip");
out.writeBytes("--" + boundary + "\r\n");
out.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"");
out.write(file.getName().getBytes("Shift_JIS"));
out.writeBytes("\"\r\n");
out.writeBytes("Content-Type: application/octet-stream\r\n\r\n");
BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));
int buff = 0;
while((buff = in.read()) != -1){
    out.write(buff);
}
out.writeBytes("\r\n");
in.close();

// 送信終わり
out.writeBytes("--" + boundary + "--");
out.flush();
out.close();

// レスポンスを受信 (これをやらないと通信が完了しない)
InputStream stream = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
String responseData = null;
while((responseData = reader.readLine()) != null){
    System.out.print(responseData);
}
stream.close();

テキストフィールドとファイルフィールドを送信しているところは、連続して何回でも呼べる (multipart のパートとなる)。

あと、最初にboudaryを取得してるところ (generateBoundary) は、以下のような感じで書いた。

private String generateBoundary(){
    String chars = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_";
    Random rand = new Random();
    String boundary = "";
    for(int i = 0; i < 40; i++){
        int r = rand.nextInt(chars.length());
        boundary += chars.substring(r, r + 1);
    }
    return boundary;
}

boundaryっていうのは、multipartのデータを送信するときにデータ(各パート)の区切りを表す文字列。どんな文字列でも良いが、boundary以外のデータに同じ文字列が含まれると、そこで区切られたことになってしまって良くない。

今回は、ランダムに40文字の文字列を生成しただけ。このやり方は、ものすごく運が悪いと、ファイル内のデータに含まれる文字列と重複しちゃうかも知れない。

けど、たぶん相当なレアケースだと思う (確率は計算してないけど) ので、気にしないことにした。