JSF で日本語ファイル名のファイルダウンロード?

このエントリは Java EE Advent Calendar 2013 - Adventar の 10 日目です。昨日は @emaggame さんの Like Spinning Plates: WildFly の Web 基盤、Undertow の紹介 でした。

JSF でのファイルアップロードについては検索すると結構色々ひっかかるものの、ダウンロードについてはいまいち情報が少ないような気がしないでもないので試してみました。試した環境は以下の通りです。

なお、今回は PrimeFaces のようなサードパーティコンポーネントは使用せず、素の JSF でいきます。

JSF Managed Bean

ファイルをレスポンスに突っ込む処理を実装します。 jsf 2 - How to provide a file download from a JSF backing bean? - Stack Overflow を参考にしました。 JSF2 からは HttpServletResponse を直接触るのではなく、 ExternalContext を介して操作ができるようです。とはいえサーブレットの頃とやることは基本的に同じですね。

FileStorage はファイルを取ってくるクラスかなんかだと思って下さい。 CDI でインジェクションしています。

@Named
@ApplicationScoped
public class Index {

    @Inject
    FileStorage fileStorage;

    public void download(String fileName) throws IOException {

        File file = fileStorage.getFileByName(fileName);

        FacesContext fc = FacesContext.getCurrentInstance();
        ExternalContext ec = fc.getExternalContext();
        ec.responseReset();
        ec.setResponseContentType("application/octet-stream");
        ec.setResponseHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
        ec.setResponseContentLength((int) file.length());

        output(file, ec.getResponseOutputStream());

        fc.responseComplete();

    }

    private void output(File file, OutputStream os) throws IOException {
        byte buffer[] = new byte[4096];
        try (FileInputStream fis = new FileInputStream(file)) {
            int size;
            while ((size = fis.read(buffer)) != -1) {
                os.write(buffer, 0, size);
            }
        }

    }
}
追記

コメント欄でご指摘をいただきましたが、本来は http ヘッダーに渡す値は URL エンコードする必要があります。

xhtml

普通に Managed Bean のメソッドを呼び出します。

<h:form id="form">
    <p><h:commandLink action="#{index.download('JAVA+YOU.txt')}" value="ダウンロォォォォド" /></p>
</h:form>

これでブラウザからダウンロードしてみます。

無事ダウンロードできました。

これでまぁ動くことは動くんですが、ファイル名に日本語が含まれている場合に文字化けで死にます。例えば xhtml を以下のように変更してみます。

<h:form id="form">
    <p><h:commandLink action="#{index.download('あなたとJAVA.txt')}" value="ダウンロォォォォド" /></p>
</h:form>

残念なことになってしまいました。

ファイル名をエンコードして HTTP ヘッダーに渡せば一応対応はできますが、ブラウザごとにエンコード方法が違ったりとかどうやっても無理なやつとか*1いたりしてなんか辛いのでやりたくないです。やめよう。

対策としてよく知られた?方法として、 URL の一部としてファイル名を渡すという方法があります。 http://example.com/ほげ.txt のような具合ですね。ブラウザは URL として渡されたファイル名をそのままダウンロードファイル名として使用するので、たいていのブラウザで文字化けなしでダウンロードさせることができます*2。しかし JSF は拡張子固定のため、この方法が使えません。オワタ

さて、どうするか。

仕様で日本語ファイル名を禁止すればいいんじゃね

そこはアレですよアレ、せっかく Java EE なので JAX-RS を使えばいいんじゃないでしょうか。JAX-RS は柔軟なルーティング機構を持っていますので、 URL でファイル名を使うような指定は簡単にできます。

JAX-RS の有効化

JAX-RSJSF との併用が可能です。 NetBeans を使っている場合は、 JSF のプロジェクトでおもむろに右クリックから RESTful Web サービスを追加します。 ApplicationConfigJAX-RS リソースクラスが自動生成されて JAX-RS が動くようになります。 NetBeans 便利ですね。

ApplicationConfig

中身は自動生成されるのでどうでもいいです。 ApplicationPath の値だけ確認またはお好みで変更しておきます。デフォルトでは長ったらしいのでここでは "rs" に変更しています。

@javax.ws.rs.ApplicationPath("rs")
public class ApplicationConfig extends Application {
    // 中身略
}

JAX-RS リソースクラス

JSF の Managed Bean と同様、こちらも CDI でのインジェクションが使えます。 Managed Bean にロジックをごりごり書いてさえいなければ、ロジックの再利用が可能です。

@Path("files")
@ApplicationScoped
public class DownloadResource {

    @Inject
    FileStorage fileStorage;

    @GET
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    @Path("{fileName}")
    public Response download(@PathParam("fileName") String fileName) {
        return Response.ok(fileStorage.getFileByName(fileName))
                .header("Content-Disposition", "attachment;")
                .build();
    }
}

ここまでの実装で {contextPath}/rs/files/ファイル名 というパスでダウンロード処理を呼び出せるようになります。

xhtml

リンクの指定を普通の html タグに変更します。

<p><a href="#{request.contextPath}/rs/files/あなたとJAVA.txt">ダウンロォォォォド</a></p>

結果

文字化けせずにダウンロードできました。

まとめ

はい、なんかタイトル詐欺っぽい感じですいません。

基本は JSF で作っていても、部分的に JAX-RS が適用できることがわかりました。 JSF では融通が利かない部分をこのように一部 JAX-RS で補うという方法もまぁありなんじゃないかなーと思いました。それはまずいんじゃ的なのがあれば教えていただけると嬉しいです。

Java EE Advent Calendar 2013 、明日はいよいよ Yoshio Terada さんです。よろしくお願いします!

*1:昔の Safari はどうエンコードしても無理だった記憶が。今はどうなのかな?

*2:だめなブラウザあるのかな?今のところ知らない。