Servlet Garden

Java, Web Application and beyond

Flower

Archive for October, 2008

How to upload files by Servlet [revival]

手元にとってあるServlet Gardenの古いドキュメントを眺めていたら、なつかしいファイルアップロードがありました。今はフレームワークを使えばなんでもそろっていて、自分でごりごりプログラムを書く必要なんか無いのですが、基本的な仕組みを理解するのに役にたつかも、と、思ったので復活させてみました。

いくつかリンク先が無くなっていたりするのですが、タグを修正する程度にして、昔そのままを再現しています。昔は若かったなぁ>自分、、、みたいな写真まであって、、、この古さをお楽しみくださいませ。

じゃばこプロジェクト(http://javaco.org/)って、今でも健在なんですね。うれしいじゃば。

2001.09.05(初公開)
2001.09.07(バグフィックス)
2001.10.06(追記)
2003.03.19(おたよりのページ公開)
2008.10.25(復刻版公開)

ファイルをアップロードしてみんとす


「RFC1867 Form-based File Upload in HTML」! なのだ

人気モノなんです(え?ワタクシ?お〜っ、ほっ、ほっ、ほ(女王様の高笑い))。。。。あぁ、無論、ファイルアップロードですよ。そう、アップロード。人気です。
実は、こ〜んなところですでに公開しているのでした。

「RFC 1867 Form Based File Upload」

http://www.java-conf.gr.jp/wg_bof/servlet/docs/980403/FileUpload.doc.html

かつての Java カンファレンス、Servlet BOF 名になっているんだけれど、ワタクシが書いたもの、作ったシロモノなのだ(と、告白してみる。特に意味は無い)。98年4月に公開した古ぅ〜いドキュメントなのに、未だに人気モノらしく、Google で “File Upload” をキーワードに”日本語のページ” 指定でサーチすると 2番目にヒットするんだなぁ…(苦笑)。さすがに 1位は Perl だけれど、PHP やそのたもろもろを押えて、サーブレットが堂々 2位っていうのはうれしいけれどね。

でも、やっぱ、古フル、手垢でうす汚れて、ところどころにホツレがみられるアレ()の姿がソーゾーされるじゃば。。。それに、今になって眺めるとこんなの必要なかったよねっていうプログラミングが盛りだくさんだから、あぁはずかしぃ〜、見ないでぇ〜。極め付けは Unix しか知らない子ちゃんだった当時のワタクシのこと、動作確認はサーバもクライアントまでも Unix のみ、ファイルセパレータに “\(バックスラッシュ)” なるものが存在するだなんてこれっぽっちも存じあげませんでしたわの時代、ファイル名に日本語が使われるプラットフォームがあるなんて異次元世界ですわのころ、、、当然のことながら不具合が発生いたしております。

それにしても、インナークラスがわからない(聞いている本人はインナークラスがわからないという意識はありません。)ことに纏わる質問の多いことったら。そのクラスのファイルが無いとか、そのクラスのコンパイルができないとか。う〜ん。
難しいですか? インナークラスって。

そこで、もろもろのバグを払拭、加えて今ふぅ〜な JSP も採り入れ、「ファイルアップロード再び」、をお届けすることにしました。どうだぁ ! > Jason Hunter (http://www.servlets.com/) なんつって…。(この … がヨワヨワな感じだな、やっぱ。)

Step1: RFC1867 ってなぁ〜んだ
Step2: アップロードデータ解析のツボ
Step3: アップロードデータ出力のツボ
Step4: アップロードデータ表示のツボ
Step5: 世間がすなるファイルアップロードなるもの

Step1: RFC1867 ってなぁ〜んだ

ほほ。またしても RFC ですわよ、みなさま。通信といえば RFC, サーブレット/JSPといえば通信、コレ抜きでは語れませんわ。HTTP/1.1 を規定しているのが RFC 2616なら、 Cookie は RFC 2109、MIME(Multipurpose Internet Mail Extension) はRFC 2045 の Part One から 2049 の Part Five までですわ。そして、ファイルをクライアントからサーバにアップロードするための RFC は 1867 ですの。アップロードするプログラムを作るなら、この RFC をお読みなさいませ。

「けっ、まぁた英語かい、読んでらんねぇよ 」なんて聞こえてきそう :-) 。大丈夫、世の中にはありがた〜い人っているものです。こんなサイトがありました。


RFC1867 がここにあるわけではないけれど、こんなサイトも。ご覧くださいませ。

そもそもアップロードって…

どうすればいいのかなぁ、と疑問に思いませんか? Web サーバとクライアント(ブラウザ)の関係ってフツーはこんなふうにサーバからクライアントに*ダウンロード* するだけだよね。

ダウンロード

ダウンロード

アップロードするっていうことはこの逆で、こんなふうにクライアントからサーバにファイルを送り込むことになる、と。

アップロード

アップロード


“あれっ、ヘンだな” と思ったアナタ、 HTTP を知っていますね。そう、サーブレット/JSP との通信に使われる HTTP はクライアントが働きかけないことにはサーバはなにも応答できません。正確にはこんなやりとりが行われるハズ。

けらい「女王様、こちらのファイルをいただきたいのですが
        いかがでございましょう。」
              (HTTP リクエスト)
女王様「よろしいですわ。差し上げましてよ。
        そのかわり、アレとソレをわたくしに渡すのです。」
              (HTTP レスポンス、HTML フォームをあげた)
けらい「ははぁ。こちらにござります。」
              (HTTP リクエスト、フォームに入力済み)
女王様「お〜、ほっほっほ」
              (HTTP レスポンス)

でも、ただのフォームじゃアップロードはできません。フォームにパラメータを入力しただけ、つまり、フォームにけらいが持っているファイルの場所を書きこんだだけじゃぁだめ。女王様はわざわざけらいのところに行ってファイルを持ち出すようなマネは間違ってもやらないのです。女王様はけらいが「こちらにござりまする。」と差し出したモノは受け取るけれど、自ら腰を上げて取りには行かない。ついでに、女王様はけらいが「いかがでございましょう。」とお伺いに行かない限り何もしない。それが HTTP サーバといふものです。


アップロードできるフォームって…

どんなフォームだったらアップロードできるのか、っていうことを書いてあるのが、
そう、何度も出てきている RFC 1867 。これによると、こんなふぅ〜。

1) INPUT 要素の TYPE 属性は FILE である。
2) FORM の ENCTYPE に “multipart/form-data” というMIME メディアタイプを指定する。


そこで、こんな JSP を作りました。

FormBasedFileUpload.jsp(一部)

<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Form Based File Upload Sample (1)</title>
</head>
<%@ page session="false"
         contentType="text/html; charset=UTF-8" %>
<%
    String contextRoot = request.getContextPath();
%>

...

<form action="<%= contextRoot %>/servlet/com.netpotlet.servlet.FileUploadServlet"
      enctype="multipart/form-data"
      method="POST">
<input type="hidden" name="encoding" value="JISAutoDetect">
<center><table border="0">
<tr>
<td align="right">お名前: </td>
<td><input type="text" name="username" size="20" maxlength="20"></td>
</tr>
<tr>
<td align="right">コメント: </td>
<td><input type="text" name="comment" size="40" maxlength="50"></td>
</tr>
<tr>
<td align="right">ファイル: </td>
<td><input type="file" size="40" name="uploadfile"></td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" value="Upload">
</td>
</tr>
</table></center>
</form>
...

するとブラウザは…

さてさて、女王様からこのような HTML を渡されたら、けらいは女王様のお気にめすように振る舞わなくてはいけません。どうしたらいいか、、、を教えてくれるのも RFC 1867 なのです。

まず、ブラウザは INPUT 要素の TYPE 属性が FILE になっていると “Browse…” ボタンを表示することになっています。日本語ブラウザでは図のように”参照…” と表示されます。このボタンをクリックするとファイルを選べるようになっています。

ファイルアップロードのフォーム

ファイルアップロードのフォーム

そして、例えばこんなふぅに入力して “Upload” ボタンをクリックする、つまり、POST するわけです、が、このとき、けらいのブラウザは女王様サーバのお気にめす形式にしてデータを送らなくちゃイケナイ。どんな形式かというと、、、これも、RFC 1867 に書いてあるのです。
(この画像は紛失しました)

いまどきのブラウザなら大抵、 RFC 1867 に対応している(いまどきでも lynx は非対応。#そんなのもちだすなって? (^^;重宝してるんですけどぉ)ので、HTTP リクエストヘッダにこんな Content-Type フィールドを付けて、

multipart/form-data; boundary=---------------------------269622935811478

こんなリクエスト本体をサーバに送る。

(空行はダテじゃないよ)

-----------------------------269622935811478
Content-Disposition: form-data; name="encoding"

JISAutoDetect
-----------------------------269622935811478
Content-Disposition: form-data; name="username"

よこ
-----------------------------269622935811478
Content-Disposition: form-data; name="comment"

お試しだよぉぉん
-----------------------------269622935811478
Content-Disposition: form-data; name="uploadfile"; filename="D:\cygwin\home\Yoko\moon\images\ちっさなアレ.JPG"
Content-Type: image/jpeg

[バイナリデータ]
-----------------------------269622935811478--

これがまさに女王様のお気にめすデータ形式で、サーバサイドではこのような形式のデータが送られて来ることを期待して解析プログラムを動かすわけです。

こういう通信の取り決めを説明しているのが 「RFC 1867 Form-based Fle Upload in HTML」なのだ!

Step2: アップロードデータ解析のツボ

どんな形式でデータが送られてくるのかわかっちゃいるんだけれど、難しいのよ、この解析が。しかも、テキストもバイナリもアップロード可なんて欲張るとね、ホンット、苦労します。何がって? java.io パッケージの API がびみょーに合わないのですよ。この手のデータ解析に。

Stream 系と Reader/Writer 系、どっち?

Java には java.io.FileInputStream, java.io.FileOutputStream のようなバイナリデータ用と、java.io.InputStreamReader/java.io.OutputStreamWriter やお馴染みの java.io.PrintWriter のようんテキストデータ用の I/O API があるよね。テキスト用は国際化されているから文字エンコーディングをちゃんと見て、そのようにバイト列を解釈してくれるものだよね。

そこで、上のブラウザが送り込んで来るデータを眺めてみると、、、うぅぅぅ〜ん。うなってしまうんだ。一見、Reader/Writer 系でいいように見えるけれど、画像やパワーポイントをアップロードすると途中からバイナリデータになってしまう。。。でも、改行コードと boundary 文字列でパートが区切られているし。。。

javax.servlet.ServletInputStream を使うのだけれど…

結局、Stream 系でしかも readLine() なメソッドが要り用です。そこで、唯一それができる javax.servlet.ServletInputStream を使う(その他の Stream 系 readLine() は deprecated になってしまったんだ)ことにあいなるわけですが、これがまた困ったちゃん。なぜか、読み込んだバイト列の最後に改行コードを付けて返してくれる。これって、Tomcat だけ?コンテナ実装の問題?

やっぱりバイナリデータは壊れるんだ

唯一の javax.servlet.ServletInputStream#readLine() を使ってバイナリデータを読むんだけれど、改行コードが付加されるのもわかるのだけれど、やっぱりバイナリデータを正しく読み取れないんです。データ中にたまたま
改行コードと同じバイト列が入っているなぁんて、あるよねやっぱり、画像なんか特に。そこで区切られてしまうと復元は *困難* のひとこと。

一つの解決方法

そこで、
<input type="file" size="40" name="uploadfile">
をフォームの中で最後の位置に持ってくることにしました。あ、submit よりは前で大丈夫よ。こうすると、アップロードされる本体のバイナリデータは一番最後のパートに入ってきます。だから、もうこの先にはバイナリデータと終了 boundary 文字列しか無いと判断した時点で入力ストリームを java.io.DataInputStream に切替えるのです。
無論、そのような実装になっていないブラウザからアップされるとこの手法は破綻するのですが、幸い、IE と Netscape Navigator (Win系)は大丈夫のよう。
HTTP リクエストヘッダの Content-Length フィールドの値とバイナリデータにたどりつくまでに読み込んだバイト数を数えておくと、バイナリデータのサイズを算出できるのでその分を読み込む努力をするわけです。

という解析を行っているのが com.netpotlet.upload.MultipartParser クラスです。このクラスは boundary 文字列に区切られた各パートをcom.netpotlet.upload.Part クラス型にした後、java.util.Vector 型の入れモノに詰め込んでいます。

画像やパワーポイントのようなバイナリデータはこれでうまく読み込めるんだけど、テキストデータをアップロードすると終了 boundary 文字列が取れていないんだな。ま、テキストだからいっか。

うわさによれば Mac の何かはだめらしい

Mac 持っていないんで、うわさを聞いただけです。Mac の何かのブラウザは区切りになる改行コードをちゃんと送り込んでこないらしい。

こういうとき女王様は「話し方がなっていませんわ!」と激怒してよいハズ。だって RFC をちゃんと実装していないんだもの。Interal Server Error の変わりに 「女王様はご機嫌を損ねた」とでも返すように設定してみようかしらん。

[追記]
(2001.09.07)
MultipartParser.java 一ヶ所、Part.java の setContent()メソッドを修正して、アップロードファイルの最後に終了バウンダリが追加されるバグをフィックスしました。
よく見たら、テキストだけではなく、画像などのバイナリファイルの最後にも終了バウンダリ文字列が付いていました。画像もパワーポイントもこれが付いていてもブラウザ経由で正常に見えていたので、気が付きませんでした。
これでテキストも最後に終了バウンダリ文字列が付かなくなりました。
MultipartParser.java
package com.netpotlet.upload;

/**
 * A class to parse an RFC 1867 request.
 *
 * @auther Yoko Kamei Harada, Copyright © 2001-2008
 * @revision 1.0.1 2001/09/07
 * @version 1.0, 2001/09/05
 *
 */

import java.io.DataInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import javax.servlet.ServletInputStream;

public class MultipartParser {
    private static String boundaryKey = "boundary=";
    private static int separatorSize = 2;
    private String boundary = null;
    private String endBoundary = null;
    private Vector eachParts = new Vector();
    private String contentType = null;

    public Vector getEachParts() {
        return eachParts;
    }

    public MultipartParser(String contentType,
                           int contentLength,
                           ServletInputStream istream)
        throws IOException {
        retrieveBoundary(contentType);
        retrieveParts(contentLength, istream);
    }

    private void retrieveBoundary(String contentType) {
        contentType = contentType.toLowerCase();
        int pos =
            contentType.indexOf(boundaryKey)+
            boundaryKey.length();
        this.boundary = "--"+contentType.substring(pos);
        this.endBoundary = "--"+contentType.substring(pos)+"--";
    }

    private void retrieveParts(int contentLength,
                               ServletInputStream istream)
        throws IOException {

        byte[] buf = new byte[8*1024];
        byte[] content;
        int ret;
        int readbytes = 0;
        int paramsLength = 0;
        int eachContentLength = 0;
        boolean contentFlag = false;
        String line;
        Part eachPart = null;
        DataInputStream dataInStream =  new DataInputStream(istream);
        while ((ret = istream.readLine(buf, 0, buf.length)) > -1) {
            content = new byte[ret];
            System.arraycopy(buf, 0, content, 0, ret);
            line = new String(content);
            if (line.indexOf(endBoundary) >=0)
                ;
            if (line.indexOf(boundary) >=0) {
                readbytes += ret;
                if (eachPart != null) {
                    eachPart.setParamsLength(paramsLength);
                    eachPart.setContentLength(eachContentLength);
                    eachParts.add(eachPart);
                }
                eachPart = new Part();
                contentFlag = false;
                paramsLength = 0;
                eachContentLength = 0;
            } else if (line.length() == separatorSize) {
                readbytes += ret;
                contentFlag = true;
                if (eachPart.isBinary()) break;
            } else if (!contentFlag) {
                readbytes += ret;
                paramsLength += ret;
                eachPart.setParams(line);
            } else if (contentFlag) {
                readbytes += ret;
                eachContentLength += ret;
                eachPart.setContent(content);
            }
        }
        int size =
            contentLength - readbytes - endBoundary.length()-4;
        eachPart.setContentLength(size);
        int bufsize = getBufSize(size);
        buf = new byte[size];
        int readsize;
        while((readsize = dataInStream.read(buf, 0, size)) > -1) {
            content = new byte[readsize];
            System.arraycopy(buf, 0, content, 0, readsize);
            eachPart.setContent(content);
        }
        eachPart.setParamsLength(paramsLength);
        eachPart.setContentLength(size);
        eachParts.add(eachPart);
        dataInStream.close();
    }

    private int getBufSize(int size) {
        int b = size / 1024;
        return ((b+1)*1024);
    }

}


Part.java
package com.netpotlet.upload;

/**
 * A class to hold each Part data of an RFC 1867 request.
 *
 * @auther Yoko Kamei Harada, Copyright © 2001-2008
 * @revision 1.0.1 2001/09/07
 * @version 1.0, 2001/09/05
 *
 */

import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Hashtable;
import java.util.StringTokenizer;

public class Part {
    private static String dataType = "Content-Disposition: form-data; ";
    private static String mimeType = "Content-Type: ";
    private boolean binary = false;
    private int paramsLength = 0;
    private int contentLength = 0;
    private int writebytes = 0;
    private String key = null;
    private Hashtable params = new Hashtable();
    private ByteArrayOutputStream ostream = new ByteArrayOutputStream();

    public Hashtable getParams() {
        return params;
    }

    public boolean isBinary() {
        return binary;
    }

    void setParams(String paramString) {
        params.put("Content-Type", "text/plain");
        if (paramString.startsWith(mimeType)) {
            params.put("Content-Type",
                       trimln(substring(paramString, mimeType)));
            return;
        }
        String param = substring(paramString, dataType);
        StringTokenizer st = new StringTokenizer(param, "; ");
        int count = st.countTokens();
        if (count < 1) return;
        StringTokenizer st2;
        String key, value;
        for (int i=0; i<count; i++) {
            st2 = new StringTokenizer(st.nextToken(), "=");
            if (st2.countTokens() == 2) {
                key = st2.nextToken();
                value = trim(st2.nextToken());
                if ("name".equals(key)) {
                    this.key = value;
                }
                if (this.key != null) {
                    params.put(this.key, value);
                }
            }
        }
        if (count < 2) {
            binary = false;
        } else {
            binary = true;
        }
    }

    void setParamsLength(int paramsLength) {
        this.paramsLength = paramsLength;
    }

    int getParamsLength() {
        return paramsLength;
    }

    void setContentLength(int contentLength) {
        this.contentLength = contentLength;
    }

    int getContentLength() {
        return contentLength;
    }

    private String substring(String str, String key) {
        if (str.indexOf(key) == 0) {
            return str.substring(key.length());
        }
        return str;
    }

    private String trim(String str) {
        if (str.endsWith("\n")) {
            return str.substring(1, str.length()-3);
        } else {
            return str.substring(1, str.length()-1);
        }
    }

    private String trimln(String str) {
        if (str.endsWith("\n")) {
            return str.substring(0, str.length()-2);
        }
        return str;
    }

    void setContent(byte[] buf)
        throws UnsupportedEncodingException {

        if (binary) {
            if ((writebytes+buf.length) <= contentLength) {
                ostream.write(buf, 0, buf.length);
                writebytes += buf.length;
            } else {
                ostream.write(buf, 0, (contentLength-writebytes));
            }
        } else {
            String value = new String(buf);
            params.put(key, trimln(value));
        }
    }

    public byte[] getBinaryContent() {
        return ostream.toByteArray();
    }

    Part() {
    }
}

Step3: アップロードデータ出力のツボ

アップロードデータの解析が終ったらファイルに出力、だね。ここもまたいろいろあるんだ。

困るんです、日本語のファイル名

そうなんです。あるんだよね、Unix じゃないプラットフォームだと日本語のファイル名が。しかも、当然のように使われているんですよ。

日本語のファイル名のまま、Unix なサーバにアップロードされる、そのファイル名で Unix ファイルシステムに書き込んでしまう。。。出来てしまうんですよ、Java を使っていると。Unix じゃないプラットフォームな人からもらった圧縮ファイルをうっかり展開したときにもこういう目に会うんだよねぇ。で、どうなるかというと、Unix 的にはファイル名が何なのかなんて化け化けの文字が並んでいるだけでさっぱりわからないし、*そのファイル* って指定はできないし、だからファイル指定で削除もできない。結局、必要なファイルを他のディレクトリに移動させてそのディレクトリごと削除するとか、いろいろ工夫することになる。。。泣ける。(注:今どきのLinuxなら日本語のファイル名も問題ありません。時代もOSも変わりました。)

「日本語のファイル名を使うな」とも言えないから、アップロードされたときのファイル名はさっくり捨てることにしました。といってもムゲに捨てるのも忍びないので表示するときにだけ使うことにしました。

サーバに書き込むときのファイル名の決め方は…

このプログラムではサーブレットの初期値から[Web アプリケーションの中のディレクトリ名/ファイルのプレフィクス]を貰うことにしました。ただ、初期値の設定がなくてもいいようにディフォルトを/data/uploadfile にしてあります。

それともう一つ。マルチスレッドで動くんだよねぇ、サーブレット/JSP って。だから、同時に 10 ファイル(10アクセス)くらいは耐えられるように 0-9 までの数字を付けることにしました。

さらに、もう一つ、拡張子。アップロードされたファイル名を見ないことにしたので、アップロードデータに書き込まれている Content-Type から拡張子を決めることにしました。Content-Type から拡張子のマッピングにはApache 付属の mime.types ファイルを使ってます。このファイルへのパスもサーブレットの初期値で指定するようにしたけれど、設定なしでも使えるようにWeb アプリケーションの中の conf ディレクトリにコピーしてきて、ディフォルトではこれを見るようにしました。

だから、ブラウザが参照するときのパス名はこんなカンジ。

http://[hostname]:[port]/[Web アプリケーション参照名]/data/uploadfile.0.jpeg

ディレクトリのパーミッションは大丈夫?

Win 系の OS だとこのへん気にするひつようはないけれど、Unix の場合、ファイルを書き込もうとしているプロセスを誰が動かしているのか、書き込もうとしているディレクトリのオーナやパーミッションがどうなっているのかってことを気にしなきゃね。

面倒くさいって? だめ、だめ。それじゃワームに感染しちゃうよ。

Web サーバっていうのは常にクラッカーの標的になるところで動かなきゃイケナイ。いつなんどきセキュホ、、、あ、セキュリティホールね、が見つかって、クラックされるかわからない。クラックされたとしても、被害を広げないようにしておかなきゃ。その一つがプロセスのオーナを権限のない、たとえば nobody さんにすることでしょ。
1024 番以下のポートは root さんじゃないと使えないようになっているから Apache は HTTP サーバのディフォルト 80 番を使ときはroot さんで起動するけれどその後、プロセスのオーナを nobody さんとかだれか root じゃない他の誰かに変更する機能があるよね。root さんはどこにでもファイルを作れるけれど、どのファイルでも消せる、書き換えられる。「作れる = 消せる = 書き換えられる」を忘れないでね。

ということでサーブレットコンテナもコンフィグファイルやディレクトリオーナと別のユーザ名で動かすといいね。そのときにはプロセスオーナに対してパーミッションを開放するディレクトリがいくつかあるけれど。

Web アプリケーションのディレクトリはちょっとしたコンフィグは必要だけれど、だれのどこのディレクトリでもかまわない。サーブレットが書き込みをするときにディレクトリのパーミッションを気にすればいいだけ。

Step4: アップロードデータ表示のツボ

サーバ上にファイルを書き込めればファイルアップロードそのものはすでに終っているから、女王様サーバは「お〜、ほっほっほ」とでもレスポンスしておけばイイ、、、じゃ、あんまりだね。

ということで最後の表示、いってみよう!

やっぱ JSP でしょ。

いまどきサーブレットのプログラムに HTML を println() なんかしないよねぇ。ということで、JSP を使って最後の表示ページを作りました。

先日開催した Servlet WG ミーティングの申し込みでちょっとしたアンケートを取りました(第14回ミーティング)。それによると「どーでもいい」も多いけれど、「JSP&サーブレット派」が多数。熱烈な「サーブレットだけ派」が数人、「JSP派」がほんの少しでした。JSP 使いが増えましたネ。
ま、「サーブレットだけ派」っていうのは自分で JSP の代わりになるツールを作ってしまった人たちばかり。元々 JSP なんて必要なかったので使っていないだけです。サーブレットに println() してるわけじゃアリマセン。

こんな JSP と JavaBeans を作って…

UploadFile.jsp
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Form Based File Upload Sample (2)</title>
</head>
<%@ page session="false"
         contentType="text/html; charset=UTF-8" %>
<%
    String contextRoot = request.getContextPath();
%>
<jsp:useBean id="uploadPropertyBean"
             type="com.netpotlet.upload.UploadPropertyBean"
             scope="request" />
<body bgcolor="#f0ffff" text="703070">
<blockquote>
+++ このファイルがアップロードされました +++<br />
<br />
<table border="0">
<tr>
<td align="right" nowrap>お名前: </td>
<td><jsp:getProperty name="uploadPropertyBean" property="username" /></td>
</tr>
<tr>
<td align="right" nowrap>コメント: </td>
<td><jsp:getProperty name="uploadPropertyBean" property="comment" /></td>
</tr>
<tr>
<td align="right" valign="top" nowrap>ファイル名: </td>
<td>
<jsp:getProperty name="uploadPropertyBean" property="uploadfile" />
<%
    if ((uploadPropertyBean.getContentType()).startsWith("image")) {
%>
<br />
<img src="<%= contextRoot %><%= uploadPropertyBean.getBinaryname() %>" />
<%
    } else {
%>

<a href="<%= contextRoot %><%= uploadPropertyBean.getBinaryname() %>">
参照
</a>
<%
    }
%>
</td>
</tr>
</table>
<br /><br />
アップロードのお試しは
<a href="<%= contextRoot %>/samples/FormBasedFileUpload.jsp">
ここから</a> 。
<blockquote>
</body>
</html>


UploadPropertyBean.java
package com.netpotlet.upload;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;

public class UploadPropertyBean implements Serializable {
    private String contentType = null;
    private String encoding = null;
    private String username = null;
    private String comment = null;
    private String uploadfile = null;
    private String binaryname = null;

    void setContentType(String contentType) {
        this.contentType = contentType;
    }

    public String getContentType() {
        return contentType;
    }

    void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    String getEncoding() {
        return encoding;
    }

    void setUsername(String username) {
        this.username = username;

    }

    public String getUsername()
        throws UnsupportedEncodingException {
        return normalize(new String(username.getBytes("ISO-8859-1"),
                                    encoding));
    }

    void setComment(String comment) {
        this.comment = comment;

    }

    public String getComment()
        throws UnsupportedEncodingException {
        return normalize(new String(comment.getBytes("ISO-8859-1"),
                                    encoding));
    }

    void setUploadfile(String uploadfile) {
        this.uploadfile = uploadfile;
    }

    public String getUploadfile()
        throws UnsupportedEncodingException {
        return getOriginalName();
    }

    private String getOriginalName()
        throws UnsupportedEncodingException {
        String original =
            new String(uploadfile.getBytes("ISO-8859-1"), encoding);
        int index = original.lastIndexOf("/");
        if (index > 0) return original.substring(index+1);
        index = original.lastIndexOf("\\");
        if (index > 0) return original.substring(index+1);
        return original;
    }

    void setBinaryname(String binaryname) {
        this.binaryname = binaryname;
    }

    public String getBinaryname() {
        return binaryname;
    }

    private String normalize(String s) {
        if (s == null) return "";

        StringBuffer ret = new StringBuffer();
        int len = s.length();
        for (int i = 0; i < len; i++) {
            char ch = s.charAt(i);
            switch (ch) {
            case '<':
                    ret.append("&#060;");
                    break;
                case '>':
                    ret.append("&#062;");
                    break;
                case '&':
                    ret.append("&#038;");
                    break;
                case '"':
                    ret.append("&#034;");
                    break;
                case '\'':
                    ret.append("&#039;");
                    break;
                case ',':
                    ret.append("&#044;");
                    break;
                case ';':
                    ret.append("&#059;");
                    break;
                case '~':
                    ret.append("&#126;");
                    break;
                case '%':
                    ret.append("&#037;");
                    break;
                case '*':
                    ret.append("&#042;");
                    break;
                case '\\':
                    ret.append("&#092;");
                    break;
                case ' ':
                    ret.append("&#160;");
                    break;
                    /*
                case '\t':
                    ret.append("&#009;");
                    break;
                    */
               default:
                    ret.append(ch);
            }
        }
        return new String(ret);
    }

    public UploadPropertyBean() {}
}

表示させるとこ〜んなふぅ〜。

実行結果

実行結果

やっぱ、JSP のスクリプトレットはみにくいなぁ。普段はカスタムタグを使うんだけど、ここでカスタムタグを使うと話が長〜くなっちゃうからスクリプトレットで手をうっときましょ。

JavaBeans はサーブレット側でインスタンスを生成して、リクエストの属性に付けてくるものを JSP 側では使うだけ。だから、<jsp:useBean … /> 要素では class じゃなくて type 属性だよ。無いからって JSP の中で JavaBeans のインスタンスが生成されても意味ナシ > class 属性。

変換しなきゃイケナイ文字たち

さて、JavaBeans に作り込んである normalize() メソッド。フォームから入力された文字を扱うときにはコレが重要、ジュウヨウ、カナラズジッコウスベシ。

ほんとうに必要なのは “<” と “>” 、せいぜい “‘” (DB に書き込む場合は必須) くらいだけれどね。他は調べたから忘れないようにつけたしておいた程度。(この変換の元ソースはもらいモノ from Shin。ありがとぉ。)

なぜ重要かって?クロスサイトスクリプティング問題回避のため、だよ。参照ぉ。
http://SecurIT.etl.go.jp/
たったコレだけだけど、かなり有効だよ。”ヤラレタぜぇ > ひろみちゅ@(´∀`)” と反省する前に、”お〜、まい、がぁ〜っと” と叫ぶ前に、被害が発生するまえに手を打っておこう!

— いじょ。アップロードでした。

★★

ソースコード一式はココからダウンロード可。

[おことわり]
このプログラムを動かししたためになんらかの被害が発生しても当方は一切感知いたしません。ご自分の責任で動かしてください。

[バグフィックス](2001.09.07)

アップロードしたファイルの最後に終了バウンダリ文字列が付いていたので、これを取る修正を行いました。

[追記](2001.10.06)

“やはり終了バウンダリが付いています” という報告がありました。みなさまからアップロードされてくるデータのかずかずをチェックすると、、、ひとつだけありました。ログから判断すると、この症状がでるUser-Agent は
Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)(Windows 98 の IE5.5 ですね)のようです。

od コマンドなどを使ってデータを眺めると、どうも終了バウンダリの書式が違うような感じです。(RFC 違反か?) アップロードファイルの先頭は間違っていないことからすると、終了以外のバウンダリは RFC で規定されているとおりに付けてきたのでしょう。う〜ん。

★★

Step5: 世間がすなるファイルアップロードなるもの

さぁて、終った終った、、、っていっても、ここで紹介した方法は一つの解法です。いつでもうまくいくとは限りません。世間がすなるファイルアップロードなるものがいくつか公開されてるからそっちも見てね。

* やっぱ、探しましょうよ。せっかくのサーチエンジンなんだから。”Upload” とか “RFC1867″ とか、ね。
http://www.google.com/
* 世界で一番有名なサーブレットによるファイルアップロードがコレ。Jason Hunter 氏の作。
http://www.servlets.com/cos/index.html
* そして「ひよこサーブレット」の “ファイルアップロード”
http://homepage3.nifty.com/uzblend/servlet/


それでは、またぁ〜。

「アレ」というのは「じゃばこぷろじぇくと」(http://javaco.org/) のメンバーが彼を指すときに使う人称代名詞じゃば。ジャ花言葉 から勝手にリンクしたら、じゃばこぷろじぇくとからも勝手にリンクされたじゃば。



…アレの着ぐるみより、じゃばこコスプレの方がウレシイなどど言ってみたり。


by よ〜これっと あっとまーく じーめいる だっと かむ