ファイルダウンロード
================================================================================

.. only:: html

.. contents:: 目次
  :depth: 3
  :local:

|

Overview
--------------------------------------------------------------------------------

| 本節では、Springでクライアントにサーバからファイルをダウンロードする機能について説明する。
| Spring MVCのViewで、ファイルのレンダリングを行うことを推奨する。

  .. note::

    コントローラクラスで、ファイルレンダリングのロジックを持たせることは推奨しない。

    理由としては、コントローラの役割から逸脱するためである。
    また、コントローラから分離することで、Viewの入れ替えが、容易にできる。

ファイルのダウンロード処理の概要を、以下に示す。

#. DispatcherServletは、コントローラへファイルダウンロードのリクエストを送信する。
#. コントローラは、ファイル表示の情報を取得する。
#. コントローラは、Viewを選択する。
#. ファイルレンダリングは、Viewで行われる。

| SpringベースのWebアプリケーションで、ファイルをレンダリングするため、本ガイドラインでは、カスタムビューを実装することを推奨する。
| Spring フレームワークでは、カスタムビューの実装に\ ``org.springframework.web.servlet.View``\ インタフェースを提供している。
|
| \ **PDFファイルの場合**\
| Springから提供されている\ ``org.springframework.web.servlet.view.document.AbstractPdfView``\ クラスは、modelの情報を用いてPDFファイルをレンダリングするときに、サブクラスとして利用するクラスである。
|
| \ **Excelファイルの場合**\
| Springから提供されている\ ``org.springframework.web.servlet.view.document.AbstractXlsxView``\ クラスは、modelの情報を用いてExcelファイルをレンダリングするときに、サブクラスとして利用するクラスである。
|
| Spring では上記以外にも、いろいろなViewの実装を提供している。
| Viewの技術詳細は、\ :url_spring_reference:`Spring Framework Documentation -View Technologies- </web/webmvc-view.html>`\ を参照されたい。

| 共通ライブラリから提供している、\ ``org.terasoluna.gfw.web.download.AbstractFileDownloadView``\ は、任意のファイルをダウンロードするために使用する抽象クラスである。
| PDFやExcel形式以外のファイルをレンダリングする際に、本クラスをサブクラスに定義する。

.. tip::
   
  ファイルダウンロード機能を提供する際には、ディレクトリトラバーサル攻撃への対策が必要な場合がある。

  ディレクトリトラバーサル攻撃については、\ :ref:`file-upload_security_related_warning_points_directory_traversal` \ を参照すること。

|

How to use
--------------------------------------------------------------------------------

PDFファイルのダウンロード
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

| PDFファイルのレンダリングには、Springから提供されている、\ ``org.springframework.web.servlet.view.document.AbstractPdfView``\ を継承したクラスを作成する必要がある。
| コントローラでPDFダウンロードを実装するための手順は、以下で説明する。
|

カスタムViewの実装
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

\ **AbstractPdfViewを継承したクラスの実装例**\

.. code-block:: java

  @Component  // (1)
  public class SamplePdfView extends AbstractPdfView {  // (2)

      @Override
      protected void buildPdfDocument(Map<String, Object> model,
              Document document, PdfWriter writer, HttpServletRequest request,
              HttpServletResponse response) throws Exception {  // (3)

          document.add(new Paragraph(model.get("serverTime").toString()));
      }
  }

.. 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)
    - | \ ``AbstractPdfView``\ を継承する。
  * - | (3)
    - | \ ``buildPdfDocument``\ メソッドを実装する。

| \ ``AbstractPdfView``\ は、PDFのレンダリングに、\ :url_openpdf:`OpenPDF <>`\ を利用している。
| そのため、Mavenのpom.xmlに OpenPDFの定義を追加する必要がある。

.. code-block:: xml

  <dependencies>
      <!-- omitted -->
      <dependency>
          <groupId>com.github.librepdf</groupId>
          <artifactId>openpdf</artifactId>
      </dependency>
  </dependencies>
  
|

.. _viewresolver-label:

ViewResolverの定義
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
\ ``org.springframework.web.servlet.view.BeanNameViewResolver``\ とは、Springのコンテキストで管理されたbean名を用いて実行するViewを選択するクラスである。

\ ``BeanNameViewResolver``\ を使用する際は、通常使用する、

* JSP用の\ ``ViewResolver``\ (\ ``InternalResourceViewResolver``\ )

より先に\ ``BeanNameViewResolver``\ が実行されるように定義する事を推奨する。

.. note::

  Spring Frameworkはさまざまな\ ``ViewResolver``\ を提供しており、複数の\ ``ViewResolver``\ をチェーンすることができる。

  そのため、特定の状況では、意図しないViewが選択されてしまうことがある。

  この動作は、\ ``<mvc:view-resolvers>``\ 要素の子要素に、優先したい\ ``ViewResolver``\ を上から順に定義する事で防ぐことができる。

|

.. tabs::
  .. group-tab:: Java Config

    .. tabs::
      .. group-tab:: JSP
    
        \ **bean定義ファイル**\
        
        .. code-block:: java
        
          @EnableAspectJAutoProxy
          @EnableWebMvc
          @Configuration
          public class SpringMvcConfig implements WebMvcConfigurer {
  
              // omitted
  
              @Override
              public void configureViewResolvers(ViewResolverRegistry registry) {
                  registry.beanName(); // (1)(2)
                  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``\ を定義する。
          * - | (2)
            - | \ ``ViewResolverRegistry#beanName``\ を先に呼び出し、通常使用する\ ``ViewResolver``\ (JSP用の\ ``ViewResolver``\ )より優先度を高くする。
    
      .. group-tab:: Thymeleaf
    
        .. code-block:: java
        
          @EnableAspectJAutoProxy
          @EnableWebMvc
          @Configuration
          public class SpringMvcConfig implements WebMvcConfigurer {
  
              // omitted
  
              @Override
              public void configureViewResolvers(ViewResolverRegistry registry) {
                  registry.beanName(); // (1)(2)
                  registry.viewResolver(thymeleafViewResolver());
              }

              @Bean
              public ThymeleafViewResolver thymeleafViewResolver(
                      SpringTemplateEngine templateEngine) {

                  // omitted
              }
        
        .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
        .. list-table::
          :header-rows: 1
          :widths: 10 90
        
          * - 項番
            - 説明
          * - | (1)
            - | \ ``ViewResolverRegistry#beanName``\ を呼び出し、\ ``BeanNameViewResolver``\ を定義する。
          * - | (2)
            - | \ ``ViewResolverRegistry#beanName``\ を先に呼び出し、通常使用する\ ``ViewResolver``\ (Thymeleaf用の\ ``ViewResolver``\ )より優先度を高くする。

  .. group-tab:: XML Config

    .. tabs::
      .. group-tab:: JSP
    
        \ **bean定義ファイル**\
        
        .. code-block:: xml
        
          <mvc:view-resolvers>
              <mvc:bean-name /> <!-- (1) (2) -->
              <mvc:jsp prefix="/WEB-INF/views/" />
          </mvc:view-resolvers>
        
        .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
        .. list-table::
          :header-rows: 1
          :widths: 10 90
        
          * - 項番
            - 説明
          * - | (1)
            - | \ ``<mvc:bean-name>``\ 要素を使用して、\ ``BeanNameViewResolver``\ を定義する。
          * - | (2)
            - | \ ``<mvc:bean-name>``\ 要素を先頭に定義し、通常使用する\ ``ViewResolver``\ (JSP用の\ ``ViewResolver``\ )より優先度を高くする。
    
      .. group-tab:: Thymeleaf
    
        .. code-block:: xml
        
          <mvc:view-resolvers>
              <mvc:bean-name /> <!-- (1) (2) -->
              <bean class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
                  <!-- omitted -->
              </bean>
          </mvc:view-resolvers>
        
        .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}|
        .. list-table::
          :header-rows: 1
          :widths: 10 90
        
          * - 項番
            - 説明
          * - | (1)
            - | \ ``<mvc:bean-name>``\ 要素を使用して、\ ``BeanNameViewResolver``\ を定義する。
          * - | (2)
            - | \ ``<mvc:bean-name>``\ 要素を先頭に定義し、通常使用する\ ``ViewResolver``\ (Thymeleaf用の\ ``ViewResolver``\ )より優先度を高くする。

|

コントローラでのViewの指定
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

| \ ``BeanNameViewResolver``\ により、コントローラで"samplePdfView"を返却することで、Springのコンテキストで管理されたBeanIDにより、"samplePdfView"であるViewが使用される。

\ **Javaソースコード**\

.. code-block:: java

  @GetMapping(value = "home", params= "pdf")
  public String homePdf(Model model) {
      model.addAttribute("serverTime", new Date());
      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**\

|

.. _excelfiledownload-label:

Excelファイルのダウンロード
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| EXCELファイルのレンダリングには、Springから提供されている、\ ``org.springframework.web.servlet.view.document.AbstractXlsxView``\ を継承したクラスを作成する必要がある。
| コントローラでEXCELファイルをダウンロードさせるための実装手順は、以下で説明する。
|

カスタムViewの実装
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

\ **AbstractXlsxViewを継承したクラスの実装例**\

.. code-block:: java

  @Component  // (1)
  public class SampleExcelView extends AbstractXlsxView {  // (2)

      @Override
      protected void buildExcelDocument(Map<String, Object> model,
              Workbook workbook, HttpServletRequest request,
              HttpServletResponse response) throws Exception {  // (3)
          Sheet sheet;
          Cell cell;
 
          sheet = workbook.createSheet("Spring");
          sheet.setDefaultColumnWidth(12);

          // write a text at A1
          cell = getCell(sheet, 0, 0);
          setText(cell, "Spring-Excel test");

          cell = getCell(sheet, 2, 0);
          setText(cell, model.get("serverTime").toString());
      }

      private Cell getCell(Sheet sheet, int rowNumber, int cellNumber) {
          Row row = sheet.createRow(rowNumber);
          return row.createCell(cellNumber);
      }
            
      private void setText(Cell cell, String text) {
          cell.setCellValue(text);
      }
  }

.. 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)
    - | \ ``AbstractXlsxView``\ を継承する。
  * - | (3)
    - | \ ``buildExcelDocument``\ メソッドを実装する。

| \ ``AbstractXlsxView``\ は、EXCELのレンダリングに、\ :url_apache_poi:`Apache POI </>`\ を利用している。
| そのため、Mavenのpom.xmlに POIの定義を追加する必要がある。

.. code-block:: xml

  <dependencies>
      <!-- omitted -->
      <dependency>
          <groupId>org.apache.poi</groupId>
          <artifactId>poi-ooxml</artifactId>
      </dependency>
  </dependencies>
  
.. note::

  xlsファイル形式をサポートしたい場合は \ ``AbstractXlsView``\ を使用されたい。

  詳細は、\ :url_spring_javadoc:`AbstractXlsViewのJavaDoc </org/springframework/web/servlet/view/document/AbstractXlsView.html>`\ を参照されたい。

.. note::

  POI 5.1.0 以降のバージョンでは\ :url_log4j:`Apache Log4j v2 </>`\ を依存関係に含んでおり、POIがLog4j 2を直接使用するようになった。

  \ |framework_name|\ではロガーのAPIに\ :url_slf4j:`SLF4J </>`\ を使用しているが、APIの優先順位の関係でLog4j 2が有効になる可能性がある。

  これを回避するため、\ ``log4j-to-slf4j``\ を依存関係に含むことでSLF4Jへブリッジされるようになる。

    .. code-block:: xml

      <dependencies>
          <!-- omitted -->
          <dependency>
              <groupId>org.apache.logging.log4j</groupId>
              <artifactId>log4j-to-slf4j</artifactId>
          </dependency>
      </dependencies>
  
    .. warning:: 

      SLF4J adapter (log4j-to-slf4j-2.0.jar) とSLF4J bridge (log4j-slf4j-impl-2.0.jar) を一緒に使用すると、SLF4J と Log4j 2の間でイベントが際限なくルーティングされてしまうため注意すること。

      詳しくは、\ :url_log4j_adaptor:`Log4j 2 to SLF4J Adapter <>`\ を参照されたい。

|

ViewResolverの定義
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

設定は、PDFファイルをレンダリングする場合と同様である。詳しくは、\ :ref:`viewresolver-label`\ を参照されたい。

|

コントローラでのViewの指定
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

| \ ``BeanNameViewResolver``\ により、コントローラで"sampleExcelView"を返却することで、Springのコンテキストで管理されたBeanIDにより、”sampleExcelView”であるViewが使用される。

\ **Javaソース**\

.. code-block:: java

  @GetMapping(value = "home", params= "excel")
  public String homeExcel(Model model) {
      model.addAttribute("serverTime", new Date());
      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**\

|

任意のファイルのダウンロード
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| 前述した、PDFやEXCELファイル以外のファイルのダウンロードを行う場合、共通ライブラリが提供している、\ ``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<String, Object> model,
         HttpServletRequest request) throws IOException {

      @Override
      protected InputStream getInputStream(Map<String, Object> model,
            HttpServletRequest request) throws IOException {  // (3)
          Resource resource = new ClassPathResource("abc.txt");
          return resource.getInputStream();
      }

      @Override
      protected void addResponseHeader(Map<String, Object> model,
            HttpServletRequest request, HttpServletResponse response) {  // (4)
          response.setHeader("Content-Disposition",
                "attachment; filename=abc.txt");
          response.setContentType("text/plain");
      }
  }

.. 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を設定する。

|

ViewResolverの定義
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

設定は、PDFファイルをレンダリングする場合と同様である。詳しくは、\ :ref:`viewresolver-label`\ を参照されたい。

|

コントローラでの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``\ クラスが実行される。

.. tip::

  前述してきたように、SpringはModelの情報をいろいろなViewにレンダリングすることができる。Springでは、複数のレンダリングエンジンをサポートしており、さまざまなViewを返却することが可能である。

  詳細は、Spring の公式ドキュメント\ :url_spring_reference:`Spring Framework Documentation -View Technologies- </web/webmvc-view.html>`\ を参照されたい。

.. raw:: latex

  \newpage
