ファイルダウンロード ================================================================================ .. only:: html .. contents:: 目次 :depth: 3 :local: | Overview -------------------------------------------------------------------------------- | 本節では、クライアントにサーバからファイルをダウンロードする機能について説明する。 | ファイルのダウンロード処理の概要を、以下に示す。 #. DispatcherServletは、コントローラへファイルダウンロードのリクエストを送信する。 #. コントローラは、ファイル表示の情報を取得する。 #. コントローラは、Viewを選択する。 #. ファイルレンダリングは、Viewで行われる。 本ガイドラインでは、共通ライブラリから提供している\ ``org.terasoluna.gfw.web.download.AbstractFileDownloadView``\ を継承したカスタムViewを実装することを推奨する。 .. tip:: ファイルダウンロード機能を提供する際には、ディレクトリトラバーサル攻撃への対策が必要な場合がある。 ディレクトリトラバーサル攻撃については、\ :ref:`file-upload_security_related_warning_points_directory_traversal` \ を参照すること。 | How to use -------------------------------------------------------------------------------- .. _FileDownloadViewResolverConfiguration: ViewResolverの設定 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | カスタムViewクラスを用いてファイルをダウンロードするため、jspやthymeleafのような画面名ではなくBean名を指定する必要がある。 | blankプロジェクトから生成されたプロジェクトでは、Springのコンテキストで管理されたBean名を用いて実行するViewを選択できるように\ ``org.springframework.web.servlet.view.BeanNameViewResolver``\ が以下のように設定されている。 .. tabs:: .. group-tab:: Java Config .. tabs:: .. group-tab:: JSP \ **SpringMvcConfig.java**\ .. code-block:: java @EnableAspectJAutoProxy @EnableWebMvc @Configuration public class SpringMvcConfig implements WebMvcConfigurer { // omitted @Override public void configureViewResolvers(ViewResolverRegistry registry) { registry.beanName(); // (1) registry.jsp("/WEB-INF/views/", ".jsp"); } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``ViewResolverRegistry#beanName``\ を呼び出し、\ ``BeanNameViewResolver``\ を定義する。 | JSP用の\ ``ViewResolver``\ より先に定義することで、\ ``BeanNameViewResolver``\ の優先度を高くする。 .. group-tab:: Thymeleaf \ **SpringMvcConfig.java**\ .. code-block:: java @EnableAspectJAutoProxy @EnableWebMvc @Configuration public class SpringMvcConfig implements WebMvcConfigurer { // omitted @Override public void configureViewResolvers(ViewResolverRegistry registry) { registry.beanName(); // (1) registry.viewResolver(thymeleafViewResolver()); } @Bean public ThymeleafViewResolver thymeleafViewResolver() { // omitted } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``ViewResolverRegistry#beanName``\ を呼び出し、\ ``BeanNameViewResolver``\ を定義する。 | Thymeleaf用の\ ``ViewResolver``\ より先に定義することで、\ ``BeanNameViewResolver``\ の優先度を高くする。 .. group-tab:: XML Config .. tabs:: .. group-tab:: JSP \ **spring-mvc.xml**\ .. code-block:: xml .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ````\ 要素を使用して、\ ``BeanNameViewResolver``\ を定義する。 | JSP用の\ ``ViewResolver``\ より先に定義することで、\ ``BeanNameViewResolver``\ の優先度を高くする。 .. group-tab:: Thymeleaf \ **spring-mvc.xml**\ .. code-block:: xml .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ````\ 要素を使用して、\ ``BeanNameViewResolver``\ を定義する。 | Thymeleaf用の\ ``ViewResolver``\ より先に定義することで、\ ``BeanNameViewResolver``\ の優先度を高くする。 .. note:: Spring Frameworkはさまざまな\ ``ViewResolver``\ を提供しており、複数の\ ``ViewResolver``\ をチェーンすることができる。 ただし、読み込み順によっては意図しないViewが選択されてしまうことがある。 この動作は、\ ````\ 要素の子要素や\ ``ViewResolverRegistry``\ に、優先したい\ ``ViewResolver``\ を上から順に定義する事で防ぐことができる。 | ファイルのダウンロード ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | 共通ライブラリが提供している\ ``org.terasoluna.gfw.web.download.AbstractFileDownloadView``\ を継承したクラスを実装する。 | \ ``AbstractFileDownloadView``\ では、以下を実装する必要がある。 1. レスポンスボディへの書き込むためのInputStreamを取得する。 2. HTTPヘッダに情報を設定する。 | カスタムViewの実装 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" | テキストファイルをダウンロードする例を用いて、説明を行う。 \ **AbstractFileDownloadViewを継承したクラスの実装例**\ .. code-block:: java @Component // (1) public class TextFileDownloadView extends AbstractFileDownloadView { // (2) @Override protected InputStream getInputStream(Map model, HttpServletRequest request) throws IOException { // (3) Resource resource = new ClassPathResource("testdata/サンプル.txt"); return resource.getInputStream(); } @Override protected void addResponseHeader(Map model, HttpServletRequest request, HttpServletResponse response) { // (4) String encodedFileName = URLEncoder.encode("サンプル.txt", StandardCharsets.UTF_8); String contentDisposition = String.format("attachment; filename*=UTF-8''%s", encodedFileName); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-Disposition", contentDisposition); // (5) response.setContentType("text/plain"); // (6) } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | 本例では、\ ``@Component``\ アノテーションを使用して、component-scanの対象としている。 | 前述した、\ ``org.springframework.web.servlet.view.BeanNameViewResolver``\ の対象とすることができる。 * - | (2) - | \ ``AbstractFileDownloadView``\ を継承する。 * - | (3) - | \ ``getInputStream``\ メソッドを実装する。 | ダウンロード対象の、\ ``InputStream``\ を返却すること。 * - | (4) - | \ ``addResponseHeaderメソッド``\ を実装する。 | ダウンロードするファイルに合わせた\ ``Content-Disposition``\ や\ ``ContentType``\ を設定する。 * - | (5) - | \ ``Content-Disposition``\ を設定する。 | 上記例では、\ ``attachment; filename*=UTF-8''サンプル.txt``\ を指定しているため、\ ``サンプル.txt``\ というテキストファイルがダウンロードされる。 | 設定可能な値は\ :url_iana_assignment:`Content Disposition Values and Parameters`\ を参照されたい。 * - | (6) - | \ ``ContentType``\ を設定する。 | 上記例ではテキストファイルとして扱うため、\ ``text/plain``\ を指定している。 | 設定可能な値は\ :url_iana_assignment:`Media Types`\ を参照されたい。 | ControllerでのViewの指定 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" | \ ``BeanNameViewResolver``\ により、コントローラで\ ``textFileDownloadView``\ を返却することで、Springのコンテキストで管理されたBeanIDが\ ``textFileDownloadView``\ のViewが使用される。 \ **Javaソース**\ .. code-block:: java @GetMapping(value = "download") public String download() { return "textFileDownloadView"; // (1) } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``textFileDownloadView``\ をメソッドの戻り値として返却することで、Springのコンテキストで管理された\ ``TextFileDownloadView``\ クラスが実行される。 | Appendix -------------------------------------------------------------------------------- 動的に生成したファイルのダウンロード ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | \ ``AbstractFileDownloadView``\ は、静的なファイルだけではなく動的に生成したファイルをダウンロードさせる場合にも利用できる。 | ここでは、参考程度にPDFファイルやExcelファイルを動的に生成しダウンロードさせる例を示す。 | PDFファイルの動的な生成 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" PDFのレンダリングに\ :url_openpdf:`OpenPDF <>`\ を利用する例を紹介する。 | OpenPDFを使用するための設定 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' \ ``pom.xml``\ に OpenPDFの定義を追加する。 .. code-block:: xml com.github.librepdf openpdf ${OPENPDF_VERSION} | カスタムViewの実装 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' .. code-block:: java @Component public class SamplePdfView extends AbstractFileDownloadView { // (1) @Inject private PdfHelper pdfHelper; // (2) @Override protected InputStream getInputStream(Map model, HttpServletRequest request) throws IOException { return pdfHelper.createPdf(model); // (2) } @Override protected void addResponseHeader(Map model, HttpServletRequest request, HttpServletResponse response) { String encodedFileName = URLEncoder.encode("サンプル.pdf", StandardCharsets.UTF_8); String contentDisposition = String.format("attachment; filename*=UTF-8''%s", encodedFileName); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-Disposition", contentDisposition); // (3) response.setContentType("application/pdf"); // (4) } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``AbstractFileDownloadView``\ を継承したカスタムViewクラスを実装する。 * - | (2) - | PDFファイルを生成するためのヘルパークラスを利用する。 | ヘルパークラスの実装例は後述する。 * - | (3) - | \ ``Content-Disposition``\ を設定する。 | 上記例では、\ ``attachment; filename*=UTF-8''サンプル.pdf``\ を指定しているため、\ ``サンプル.pdf``\ というPDFファイルがダウンロードされる。 * - | (4) - | \ ``ContentType``\ を設定する。 | PDFファイルとして扱うため、\ ``application/pdf``\ を指定している。 | ヘルパークラスの実装 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' | Viewに処理を書くのではなく、ヘルパークラスで処理を実装する。 | ヘルパークラスを作成することで再利用性を高めることができる。 | 下記サンプルでは、\ ``model``\ に設定された\ ``serverTime``\ をPDFに出力する単純な例となる。 | 本サンプルで紹介していない機能を使用したい場合は、\ :url_openpdf:`OpenPDF <>`\ のサンプルやチュートリアルを参考に実装されたい。 .. code-block:: java import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Map; import org.springframework.stereotype.Component; import com.lowagie.text.Document; import com.lowagie.text.DocumentException; import com.lowagie.text.Element; import com.lowagie.text.Font; import com.lowagie.text.PageSize; import com.lowagie.text.Paragraph; import com.lowagie.text.Phrase; import com.lowagie.text.pdf.BaseFont; import com.lowagie.text.pdf.PdfWriter; @Component public class PdfHelper { public InputStream createServerTimePdf(Map model) throws IOException { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { try (Document document = new Document(PageSize.A4)) { // (1) PdfWriter.getInstance(document, outputStream); // (2) document.open(); BaseFont bf = BaseFont.createFont("HeiseiKakuGo-W5", "UniJIS-UCS2-H", false); Font titleFont = new Font(bf, 18); Paragraph paragraph = new Paragraph(new Phrase("サンプル PDF", titleFont)); paragraph.setAlignment(Element.ALIGN_CENTER); document.add(paragraph); // (3) String serverTime = model.get("serverTime") != null ? model.get("serverTime").toString() : "Server Time not available"; document.add(new Paragraph("Server Time: " + serverTime)); // (3) } catch (DocumentException e) { throw new IOException("Failed to create PDF document", e); } return new ByteArrayInputStream(outputStream.toByteArray()); } catch (Exception e) { throw new IOException("Failed to create PDF document", e); } } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``Document``\ を生成する。 | 上記例ではA4サイズのPDFを生成している。 * - | (2) - | \ ``PdfWriter``\ のインスタンスを生成し、DocumentとOutputStreamを関連付ける。 * - | (3) - | PDFドキュメントに\ ``Paragraph``\ でテキストを追加する。 | ControllerでのViewの指定 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' | \ ``BeanNameViewResolver``\ により、コントローラで\ ``samplePdfView``\ を返却することで、Springのコンテキストで管理されたBeanIDが\ ``samplePdfView``\ のViewが使用される。 \ **Javaソース**\ .. code-block:: java @GetMapping(value = "sample", params= "pdf") public String samplePdf(Model model) { model.addAttribute("serverTime", LocalDateTime.now()); return "samplePdfView"; // (1) } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``samplePdfView``\ をメソッドの戻り値として返却することで、Springのコンテキストで管理された\ ``SamplePdfView``\ クラスが実行される。 上記の手順を実行した後、以下に示すようなPDFが生成されダウンロードすることができる。 .. figure:: ./images_FileDownload/file-download-pdf.png :alt: FILEDOWNLOAD PDF :width: 60% :align: center \ **Picture - FileDownload PDF**\ | .. _filedownload_excel: Excelファイルの動的な生成 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" Excelのレンダリングに\ :url_apache_poi:`Apache POI `\ を利用する例を紹介する。 | Apache POIを使用するための設定 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' \ ``pom.xml``\ に Apache POIの定義を追加する。 .. code-block:: xml org.apache.poi poi-ooxml ${POI_VERSION} .. note:: Apache POI 5.1.0 以降のバージョンでは\ :url_log4j:`Apache Log4j v2 `\ を依存関係に含んでおり、POIがログ実装としてLog4j 2を直接使用するようになった。 Apache POIで出力されるログを\ |framework_name|\ で設定している\ :url_slf4j:`SLF4J `\ でログを出力するためには、\ ``log4j-to-slf4j``\ を依存関係に含む必要がある。 .. code-block:: xml org.apache.logging.log4j log4j-to-slf4j なお、Log4j 2を依存関係に含んでいる場合、Apache Commons Logging 1.3のログ実装呼び出し順の関係でSLF4JではなくLog4j 2が優先される可能性があるが、blankプロジェクトから生成されたプロジェクトでは\ ``commons-logging.properties``\ でSLF4Jを優先的に呼び出すように設定しているため、Log4j 2が優先されることはない。 .. warning:: SLF4J adapter (log4j-to-slf4j) とSLF4J bridge (log4j-slf4j-impl) を一緒に使用すると、SLF4Jと Log4j 2の間でイベントが際限なくルーティングされてしまうため注意すること。 詳しくは、\ :url_log4j_adaptor:`Log4j 2 to SLF4J Adapter <>`\ を参照されたい。 | カスタムViewの実装 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' .. code-block:: java @Component public class SampleExcelView extends AbstractFileDownloadView { // (1) @Autowired private ExcelHelper excelHelper; // (2) @Override protected InputStream getInputStream(Map model, HttpServletRequest request) throws IOException { return excelHelper.createServerTimeExcel(model); // (2) } @Override protected void addResponseHeader(Map model, HttpServletRequest request, HttpServletResponse response) { String encodedFileName = URLEncoder.encode("サンプル.xlsx", StandardCharsets.UTF_8); String contentDisposition = String.format("attachment; filename*=UTF-8''%s", encodedFileName); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-Disposition", contentDisposition); // (3) response.setContentType( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); // (4) } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``AbstractFileDownloadView``\ を継承したカスタムViewクラスを実装する。 * - | (2) - | EXCELファイルを生成するためのヘルパークラスを利用する。 | ヘルパークラスの実装例は後述する。 * - | (3) - | \ ``Content-Disposition``\ を設定する。 | 上記例では、\ ``attachment; filename*=UTF-8''サンプル.xlsx``\ を指定しているため、\ ``サンプル.xlsx``\ というEXCELファイルがダウンロードされる。 * - | (4) - | \ ``ContentType``\ を設定する。 | XLSXフォーマットのEXCELファイルとして扱うため、\ ``application/vnd.openxmlformats-officedocument.spreadsheetml.sheet``\ を指定している。 | ヘルパークラスの実装 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' | Viewに処理を書くのではなく、ヘルパークラスで処理を実装する。 | ヘルパークラスを作成することで再利用性を高めることができる。 | 下記サンプルでは、\ ``model``\ に設定された\ ``serverTime``\ をEXCELに出力する単純な例となる。 | 本サンプルで紹介していない機能を使用したい場合は、\ :url_apache_poi:`Apache POI `\ のHow to useを参考に実装されたい。 .. code-block:: java import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Map; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Component; @Component public class ExcelHelper { public InputStream createServerTimeExcel(Map model) throws IOException { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); Workbook workbook = new XSSFWorkbook()) { // (1) Sheet sheet = workbook.createSheet("サンプル"); // (2) sheet.setDefaultColumnWidth(12); Cell titleCell = getCell(sheet, 0, 0); titleCell.setCellValue("サンプル Excel"); // (3) String serverTime = model.get("serverTime") != null ? model.get("serverTime").toString() : "Server Time not available"; Cell timeCell = getCell(sheet, 2, 0); timeCell.setCellValue("Server Time: " + serverTime); // (4) workbook.write(outputStream); // (5) return new ByteArrayInputStream(outputStream.toByteArray()); } catch (Exception e) { throw new IOException("Failed to create Excel document", e); } } private Cell getCell(Sheet sheet, int rowNumber, int cellNumber) { Row row = sheet.getRow(rowNumber); if (row == null) { row = sheet.createRow(rowNumber); } return row.createCell(cellNumber); } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``Workbook``\ を生成する。 | 上記例ではXLSXフォーマットのEXCELファイルを生成している。 * - | (2) - | \ ``サンプル``\ シートの作成。 * - | (3) - | A1セルの値として\ ``サンプル Excel``\ を設定。 * - | (4) - | A3セルの値として\ ``Server Time: {serverTime}``\ を設定。 * - | (5) - | \ ``Workbook``\ の内容を\ ``OutputStream``\ に書き込む。 | ControllerでのViewの指定 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' | \ ``BeanNameViewResolver``\ により、コントローラで\ ``sampleExcelView``\ を返却することで、Springのコンテキストで管理されたBeanIDが\ ``sampleExcelView``\ のViewが使用される。 \ **Javaソース**\ .. code-block:: java @GetMapping(value = "sample", params= "excel") public String sampleExcel(Model model) { model.addAttribute("serverTime", LocalDateTime.now()); return "sampleExcelView"; // (1) } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``sampleExcelView``\ をメソッドの戻り値として返却することで、Springのコンテキストで管理された\ ``SampleExcelView``\ クラスが実行される。 上記の手順を実行した後、以下に示すようなEXCELが生成されダウンロードすることができる。 .. figure:: ./images_FileDownload/file-download-excel.png :alt: FILEDOWNLOAD EXCEL :width: 60% :align: center \ **Picture - FileDownload EXCEL**\ | .. raw:: latex \newpage