Servletでファイルダウンロードさせる実装のメモと、ダウンロードファイル名に関するちょっとした実験の結果です。

ダウンロードの動きを取るかどうかは最終的にはブラウザに依存するようだが、レスポンスヘッダ「Content-Disposition」に「attachment」をいれてやり、かつファイルの種類によって適切なContent-typeを指定することで、ダウンロードの動きになるようである。


静的HTMLからPOSTしてファイルをダウンロードする仕組みを簡単に実装してみる。

■Test.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>Test DownLoad</title>

	<meta HTTP-EQUIV='Cache-Control' CONTENT='no-cache'>
	<meta HTTP-EQUIV='Pragma' CONTENT='no-cache'>
	<meta HTTP-EQUIV='Expires' CONTENT='-1'>

</head>
<body>
	<form action="/downloadtest/DownLoadTestServlet" method="post">
		<input type="radio" name="rad" value="pdf">PDFファイル</br>
		<input type="radio" name="rad" value="txt">TXTファイル</br>
		<input type="radio" name="rad" value="csv">CSVファイル</br>
		<input type="radio" name="rad" value="xls">XLSファイル</br>
		<br/>
		<input type="submit" value="GO! Download!!">
	</form>
</body>

■DownLoadTestServlet

import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;
import java.io.*;
import java.text.*;
import java.net.*;
import javax.mail.internet.*;

public class DownLoadTestServlet extends HttpServlet {

	private static final DateFormat LOG_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS");

	private static final String ENCODING_UTF8 = "UTF-8";

	private static final File DOWNLOAD_DIR = new File("D:\\downloadtest");
	private static final String DOWNLOAD_TXT_FILENAME = "①髙㎞№㍑㌫アイウエオα〜£|∥①髙㎞№㍑㌫アイウエオα〜£|∥①髙㎞№㍑㌫アイウエオα〜£|∥①髙㎞№㍑㌫アイウエオα〜£|∥.txt";
	private static final String DOWNLOAD_PDF_FILENAME = "①髙㎞№㍑㌫アイウエオα〜£|∥①髙㎞№㍑㌫アイウエオα〜£|∥①髙㎞№㍑㌫アイウエオα〜£|∥①髙㎞№㍑㌫アイウエオα〜£|∥.pdf";
	private static final String DOWNLOAD_CSV_FILENAME = "①髙㎞№㍑㌫アイウエオα〜£|∥①髙㎞№㍑㌫アイウエオα〜£|∥①髙㎞№㍑㌫アイウエオα〜£|∥①髙㎞№㍑㌫アイウエオα〜£|∥.csv";
	private static final String DOWNLOAD_XLS_FILENAME = "①髙㎞№㍑㌫アイウエオα〜£|∥①髙㎞№㍑㌫アイウエオα〜£|∥①髙㎞№㍑㌫アイウエオα〜£|∥①髙㎞№㍑㌫アイウエオα〜£|∥.xls";

	public void doGet(HttpServletRequest req , HttpServletResponse res) throws IOException,ServletException {
		this.doDownload(req,res);
	}

	public void doPost(HttpServletRequest req , HttpServletResponse res) throws IOException,ServletException {
		this.doDownload(req,res);
	}

	private void doDownload(HttpServletRequest request,
	    HttpServletResponse response) throws ServletException, IOException {
	    OutputStream out = null;
	    InputStream in = null;
	    try {
	    	String choice = request.getParameter("rad");
	    	String contentType = "";
	    	String downloadfilename = "";
	    	if (choice != null) {
	    		choice = choice.trim();
	    		if (choice.equals("pdf")) {
	    			contentType = "application/pdf";
	    			downloadfilename = DOWNLOAD_PDF_FILENAME;
	    		} else if (choice.equals("txt")) {
	    			contentType = "text/plain";
	    			downloadfilename = DOWNLOAD_TXT_FILENAME;
	    		} else if (choice.equals("csv")) {
	    			contentType = "text/comma-separated-values";
	    			downloadfilename = DOWNLOAD_CSV_FILENAME;
	    		} else if (choice.equals("xls")) {
	    			contentType = "application/vnd.ms-excel";
	    			downloadfilename = DOWNLOAD_XLS_FILENAME;
	    		} else {
		    		throw new ServletException("Parameter is illegular value!! [" + choice + "].");
	    		}
	    	} else {
	    		throw new ServletException("Parameter is illegular value  [" + choice + "].");
	    	}
	    	File downloadfile = new File(DOWNLOAD_DIR,downloadfilename);

	    	response.setContentType(contentType);

	    	//String downloadFileName = new String(DOWNLOAD_FILENAME.getBytes("Windows-31J"), "ISO8859_1");
	    	String downloadfilename_foruser = new String(downloadfilename.getBytes("MS932"),"MS932");
	    	String downloadfilename_2 = new String(downloadfilename.getBytes("Windows-31J"), "ISO8859_1");

	    	System.out.println("***************************************************");
	    	System.out.println("RadioButtonChoice:" + choice + ";");
	    	System.out.println("ContentType:" + contentType + ";");
	    	System.out.println("DownlodFileName:" + downloadfilename + ";");
	    	System.out.println("DownlodFileNameForUser:" + downloadfilename_foruser + ";");
	    	System.out.println("DownlodFileName2:" + downloadfilename_2 + ";");
	    	System.out.println("***************************************************");

	        response.setHeader(
	            "Content-Disposition",
	        	"attachment; filename=\"" + downloadfilename_2 + "\"");
			response.setHeader("Cache-Control", "private");
			response.setHeader("Pragma", "");

	        in = new FileInputStream(downloadfile);
	        out = response.getOutputStream();
	        byte[] buff = new byte[1024];
	        int len = 0;
	        while ((len = in.read(buff, 0, buff.length)) != -1) {
	            out.write(buff, 0, len);
	        }
	    } finally {
	        if (in != null) {
	            try {
	                in.close();
	            } catch (IOException e) {
	            }
	        }
	        if (out != null) {
	            try {
	                out.close();
	            } catch (IOException e) {
	            }
	        }
	    }
	}

}

■ちなみにweb.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
	<servlet>
		<servlet-name>DownLoadTestServlet</servlet-name>
		<servlet-class>DownLoadTestServlet</servlet-class>
	</servlet>

	<servlet-mapping>
		<servlet-name>
			DownLoadTestServlet
		</servlet-name>
		<url-pattern>
			/DownLoadTestServlet
		</url-pattern>
	</servlet-mapping>

</web-app>

これらをwarに固めるなりなんなりしてWebアプリ化し、Tomcat起動して「Test.html」を開く。なんかラジオボタンを選択して「Go! Download!!」ボタンを押下するとダウンロードが始まる。勿論、ダウンロードファイルの取得元(この実装例だと D:\downloadtest)にちゃんとダウンロードファイルが実在することが前提である。


この実装例では対象のファイルを4種用意してあるが、ダウンロードさせるファイルの拡張子(というか種類)によって、使用するContent-typeを切り分けている。ちなみにこの実装例では以下の通りのContent-typeを指定している。

拡張子(ファイルの種類)指定するContent-type
PDFapplication/pdf
TXTtext/plain
CSVtext/comma-separated-values
XLS(Excelファイル)application/vnd.ms-excel

ファイルのダウンロードを走らせる動きはレスポンスヘッダの「Content-Disposition」に「attachment」をいれる、というのが冒頭の記述の一部であるけども、それ以外にContent-typeの指定の仕方でもブラウザがダウンロードする動きを取るかどうかが変わることがある。ここに挙げているContent-type以外でも、ブラウザ側が「ダウンロード」する動きをとることもあるし、そうでないのもある(直接ファイル開いたりとか)。言い換えれば、単にユーザに対してダウンロードさせることを目的とするのであれば、↑に挙げているContent-typeの指定が正解というわけではない(はずである)。というよりこの実装に関して「正解」という姿があるのかどうかもぶっちゃけわからない(推奨みたいなものはあるのかもしれないが)。 この辺、Content-typeに基づくブラウザ側の挙動は、どうもブラウザ任せになっているところがあり、アプリケーション側からどうこう指示できるようなものでもないようだ。


ちなみにファイル名に機種依存文字とか半角カナとかいれてるのはわざとである(テスト用である)。ダウンロードファイルの「ファイル名」には原則半角英数字しか使うべきではない、というのをどっかの掲示板の記事で見たことがある。(RFC的に規定してないとかなんとか)実際にダウンロードしたファイル名に全角文字が入っていても、それはエンコードした文字列をブラウザが解釈して”たまたま”該当するマッピングの文字が見つかったというだけで偶然の産物に過ぎないのだそうだ。

というわけなのかなんなのかわからないが、本項で取り上げているファイル名の、特に「①髙㎞№㍑㌫アイウエオα〜£|∥」にあたる部分については、ブラウザによってダウンロードさせるファイル名が異なっている。

ブラウザダウンロードファイル名備考
IE11①髙㎞№㍑㌫アイウエオα_£|∥「_」(半角アンダーバー;U+005F)になる
Fire Fox44.0.2①髙㎞№㍑㌫アイウエオα_£|∥「_」(半角アンダーバー;U+005F)になっている
Google Chrome48.0.256①髙㎞№㍑㌫アイウエオα**-**£|∥「-」(半角ハイフン;U+002D)になっている

となる。要するに波ダッシュ(U+301C)の文字の扱いがブラウザによって違う。いわゆるJavaで文字化けする文字7種は全部そうなのかな?よくわからんが。Google Chromeだけ違うってのが意外だなあ。このテのやつって大抵IEだけ村八分で「またIEか(苦笑)」みたいな思いすることが多いのだが。 どれが正解なのかはわからないが(というか多分RFC的にはどれも正解ではないのだろうが)まあこうゆうこともあるということで。