Multipart HTTP file upload with JavaFX

『Transition in JavaFX』 がやっと中日を迎えたのでちょっと一休みして...
以下のサイトに JavaFX の File Upload について紹介されていたのでちょっと試してみたのだが...
あまりにもショックなことが起こったので一応記しておこうと思う...

http://sites.google.com/site/javafxcodesamples/experiments-1/http-file-upload-with-javafx

まずは、適当にサーブレットを作ってサーバにデプロイ...
最初に 数 KB 程度の小さなテキストファイルをアップロードしてみた。問題がおこるはずもない。
当然、ちゃんとアップロードできた。メデタシ メデタシ。

サンプル程度ならこれで終了でもよいですが...
実際に使うのであれば、今時、数 MB の写真 数枚 や数十 MB の動画等をアップロードするのは普通だろう...
それなら、次は 100 MB 程度のバイナリファイルをアップロードしてみよう と言うことで...
たまたま すぐそこにあった Galileo (Eclipse 3.5) をアップロードしてみることに...

なんと OutOfMemoryError!!
なっ なにぃ... スタックトレースを見てみると、ByteArrayOutputStream の文字が...
どこの世界に アップロードするファイルを全てメモリ上に読み込む奴がいるんだ!!
と思ったら、犯人は JavaFX だった...
どうも HttpRequest#onOutput(OutputStream) の引数が ByteArrayOutputStream (のサブクラス)のインスタンスらしい。

Exception in thread "AWT-EventQueue-0" java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOf(Unknown Source)
        at java.io.ByteArrayOutputStream.write(Unknown Source)
        at FileUpload$HttpRequest.writeFiles(FileUpload.fx:95)
        at FileUpload$HttpRequest$1.invoke(FileUpload.fx:45)
        at FileUpload$HttpRequest$1.invoke(FileUpload.fx:45)
        at javafx.io.http.HttpRequest.impl_setOutput(HttpRequest.fx:1170)
        at com.sun.javafx.io.http.impl.CallbackHandler.setOutput(CallbackHandler.fx:40)
        at com.sun.javafx.io.http.impl.BaseTask$WriteNotifier.run(BaseTask.java:196)
        at com.sun.javafx.io.http.impl.desktop.DesktopProfile$3.invoke(DesktopProfile.java:167)
        at com.sun.javafx.io.http.impl.desktop.DesktopProfile$3.invoke(DesktopProfile.java:165)
        at com.sun.javafx.runtime.Entry$2.run(Entry.java:105)
        at java.awt.event.InvocationEvent.dispatch(Unknown Source)
        at java.awt.EventQueue.dispatchEvent(Unknown Source)
        at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
        at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
        at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
        at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
        at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
        at java.awt.EventDispatchThread.run(Unknown Source)

何か使い方を間違えていないかと思い、調べてみると...
以下のサイトにズバリ答えが!!

http://blogs.sun.com/rakeshmenonp/entry/javafx_upload_and_download_large
どうやら、onOutput を使う代わりに JavaFX 1.2 で追加された source 属性に InputStream を設定するだけでよいらしい。 ちなみに、ダウンロードの場合は onInput ではなく、sink 属性に OutputStream を設定すれば良い。
API リファレンスにもちゃんと書いておいてほしいなぁ。
早速、FileUpload.fxpostinitsource 属性に InputStream を設定して、writeFiles(Output) を削除してみた。

FileUpload.fx
import javafx.io.http.HttpRequest;
import java.io.File;
import java.io.OutputStream;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import javafx.io.http.HttpHeader;
import javafx.date.DateTime;
import javafx.data.Pair;

/**
 * HttpRequest able to upload files to a given url
 *
 * @author jan
 */

def PREFIX = "--";

def NEWLINE = "\r\n";

def BOUNDARY = createBoundary();

def CONTENT_TYPE = "multipart/form-data; boundary={BOUNDARY}";

def NAME = "upfile";

function createBoundary() :String {
    def now = DateTime {};
    return "{Long.toHexString(now.instant)}";
}

public class HttpRequest extends javafx.io.http.HttpRequest {

    // A sequence of files to upload
    public-init var files:File[];

    // Fields to upload together with the request
    public-init var fields:Pair[];

    postinit{

        // we always do a post
        method = HttpRequest.POST;

        // add the headers we need
        insert HttpHeader {
            name:HttpHeader.ACCEPT
            value: "*/*"
        } into headers;
        insert HttpHeader {
            name:HttpHeader.CONTENT_TYPE
            value: CONTENT_TYPE
        } into headers;
        insert HttpHeader {
            name:HttpHeader.CONNECTION
            value: "keep-alive"
        } into headers;
        insert HttpHeader {
            name:HttpHeader.CACHE_CONTROL
            value: "no-cache"
        } into headers;
        insert HttpHeader {
            name:HttpHeader.ACCEPT_CHARSET
            value: "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
        } into headers;


        var streams = new ArrayList();
        var bos = new ByteArrayOutputStream();
        var ps = new PrintStream(bos, true);

        // send the files
        if (files != null and sizeof files > 0) {

            for (file in files) {
                ps.print(PREFIX);
                ps.println(BOUNDARY);
                ps.println("Content-Disposition: form-data; name=\"{NAME}\"; filename=\"{file.getName()}\"");
                ps.println("Content-Type: application/octet-stream");
                ps.println();
                streams.add(new ByteArrayInputStream(bos.toByteArray()));
                bos.reset();

                streams.add(new FileInputStream(file));

                ps.println();
                streams.add(new ByteArrayInputStream(bos.toByteArray()));
                bos.reset();
            }

            ps.println();
            streams.add(new ByteArrayInputStream(bos.toByteArray()));
            bos.reset();
        }

        // write the fields
        if (fields != null and sizeof fields > 0) {
            for (field in fields) {
                ps.print(PREFIX);
                ps.println(BOUNDARY);
                ps.println("Content-Disposition: form-data; name=\"{field.name}\"");
                ps.println();
                ps.println("{field.value}");
            }
            streams.add(new ByteArrayInputStream(bos.toByteArray()));
            bos.reset();
        }

        ps.print(PREFIX);
        ps.print(BOUNDARY);
        ps.println(PREFIX);
        streams.add(new ByteArrayInputStream(bos.toByteArray()));
        bos.reset();

        source = new SequenceInputStream(Collections.enumeration(streams));
    }
}

これで問題なくアップロードできるだろうと思ったのも束の間...
今度は エラーも発生せず、アップロードもされず、何も起きない...
onWritten でサイズを標準出力してみたが、なぜか 30MB ちょっとまでしか表示されない。
なんてことだ!! これなら、まだ、エラーが出た方がマシじゃないか...

あくまで推測だが...
以下の記事によると URLConnection は デフォルトで ByteArrayOutputStream を使うということなので、たぶんそれが原因で OutOfMemoryError になっているのではないだろうか? 結局 最初の問題と同じ?
普段、commons-httpclient ばかり使っているので、まさか URLConnection のデフォルトが ByteArrayOutputStream だとは、思いもしなかった...

http://d.hatena.ne.jp/nacookan/20080108/1199774995
しかし、そうだとしても、OutOfMemoryError はどこに行ってしまったのだろうか? 謎は深まるばかりだ...
一体どうしたらよいのだろうか... あ〜、ソースがほしい!!

以下 変更...
変更した FileUpload.fx のソースを全部載せました。一部だとわかりにくかったので...