このエントリは 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-RS は JSF との併用が可能です。 NetBeans を使っている場合は、 JSF のプロジェクトでおもむろに右クリックから RESTful Web サービスを追加します。 ApplicationConfig
と JAX-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>