# 4.11. ファイルダウンロード

## 4.11.1. Overview

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

> Note

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

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

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

1. DispatchServletは、コントローラへファイルダウンロードのリクエストを送信する。

2. コントローラは、ファイル表示の情報を取得する。

3. コントローラは、Viewを選択する。

4. ファイルレンダリングは、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の技術詳細は、Spring Framework Documentation -View Technologies-を参照されたい。
共通ライブラリから提供している、`org.terasoluna.gfw.web.download.AbstractFileDownloadView`は、任意のファイルをダウンロードするために使用する抽象クラスである。
PDFやExcel形式以外のファイルをレンダリングする際に、本クラスをサブクラスに定義する。

Tip

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

ディレクトリトラバーサル攻撃については、ディレクトリトラバーサル攻撃 を参照すること。

## 4.11.2. How to use

### 4.11.2.1. PDFファイルのダウンロード

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

#### 4.11.2.1.1. カスタムViewの実装

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

    @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()));
        }
    }

項番 | 説明
--- | ---
(1) | 本例では、`@Component`アノテーションを使用して、component-scanの対象としている。 後述する、`org.springframework.web.servlet.view.BeanNameViewResolver`の対象とすることができる。
(2) | `AbstractPdfView`を継承する。
(3) | `buildPdfDocument`メソッドを実装する。

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

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

Note

上記設定例は、依存ライブラリのバージョンをBOMプロジェクトである terasoluna-dependencies で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。

#### 4.11.2.1.2. 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`を上から順に定義する事で防ぐことができる。

bean定義ファイル

    @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");
        }

項番 | 説明
--- | ---
(1) | `ViewResolverRegistry#beanName`を呼び出し、`BeanNameViewResolver`を定義する。
(2) | `ViewResolverRegistry#beanName`を先に呼び出し、通常使用する`ViewResolver`(JSP用の`ViewResolver`)より優先度を高くする。

    @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
        }

項番 | 説明
--- | ---
(1) | `ViewResolverRegistry#beanName`を呼び出し、`BeanNameViewResolver`を定義する。
(2) | `ViewResolverRegistry#beanName`を先に呼び出し、通常使用する`ViewResolver`(Thymeleaf用の`ViewResolver`)より優先度を高くする。

bean定義ファイル

    <mvc:view-resolvers>
        <mvc:bean-name /> <!-- (1) (2) -->
        <mvc:jsp prefix="/WEB-INF/views/" />
    </mvc:view-resolvers>

項番 | 説明
--- | ---
(1) | `<mvc:bean-name>`要素を使用して、`BeanNameViewResolver`を定義する。
(2) | `<mvc:bean-name>`要素を先頭に定義し、通常使用する`ViewResolver`(JSP用の`ViewResolver`)より優先度を高くする。

    <mvc:view-resolvers>
        <mvc:bean-name /> <!-- (1) (2) -->
        <bean class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
            <!-- omitted -->
        </bean>
    </mvc:view-resolvers>

項番 | 説明
--- | ---
(1) | `<mvc:bean-name>`要素を使用して、`BeanNameViewResolver`を定義する。
(2) | `<mvc:bean-name>`要素を先頭に定義し、通常使用する`ViewResolver`(Thymeleaf用の`ViewResolver`)より優先度を高くする。

#### 4.11.2.1.3. コントローラでのViewの指定

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

Javaソースコード

    @GetMapping(value = "home", params= "pdf")
    public String homePdf(Model model) {
        model.addAttribute("serverTime", new Date());
        return "samplePdfView";   // (1)
    }

項番 | 説明
--- | ---
(1) | “samplePdfView” をメソッドの戻り値として返却することで、 Springのコンテキストで管理された、`SamplePdfView`クラスが実行される。

上記の手順を実行した後、以下に示すようなPDFを開くことができる。

Picture - FileDownload PDF

### 4.11.2.2. Excelファイルのダウンロード

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

#### 4.11.2.2.1. カスタムViewの実装

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

    @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);
        }
    }

項番 | 説明
--- | ---
(1) | 本例では、`@Component`アノテーションを使用して、component-scanの対象としている。 前述した、`org.springframework.web.servlet.view.BeanNameViewResolver`の対象とすることができる。
(2) | `AbstractXlsxView`を継承する。
(3) | `buildExcelDocument`メソッドを実装する。

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

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

Note

上記設定例は、依存ライブラリのバージョンをBOMプロジェクトである terasoluna-dependencies で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。

Note

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

詳細は、AbstractXlsViewのJavaDocを参照されたい。

Note

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

TERASOLUNA Server Framework for Java (5.x)ではロガーのAPIにSLF4Jを使用しているが、APIの優先順位の関係でLog4j 2が有効になる可能性がある。

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

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

上記設定例は、依存ライブラリのバージョンをBOMプロジェクトである terasoluna-dependencies で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。

> Warning

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

> 詳しくは、Log4j 2 to SLF4J Adapterを参照されたい。

#### 4.11.2.2.2. ViewResolverの定義

設定は、PDFファイルをレンダリングする場合と同様である。詳しくは、ViewResolverの定義を参照されたい。

#### 4.11.2.2.3. コントローラでのViewの指定

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

Javaソース

    @GetMapping(value = "home", params= "excel")
    public String homeExcel(Model model) {
        model.addAttribute("serverTime", new Date());
        return "sampleExcelView";  // (1)
    }

項番 | 説明
--- | ---
(1) | “sampleExcelView” をメソッドの戻り値として返却することで、 Springのコンテキストで管理された、`SampleExcelView`クラスが実行される。

上記の手順を実行した後、以下に示すようなEXCELを開くことができる。

Picture - FileDownload EXCEL

### 4.11.2.3. 任意のファイルのダウンロード

前述した、PDFやEXCELファイル以外のファイルのダウンロードを行う場合、共通ライブラリが提供している、`org.terasoluna.gfw.web.download.AbstractFileDownloadView`を継承したクラスを実装すればよい。
他の形式にファイルレンダリングするために、`AbstractFileDownloadView`では、以下を実装する必要がある。

1. レスポンスボディへの書き込むためのInputStreamを取得する。

2. HTTPヘッダに情報を設定する。

コントローラでファイルダウンロードを実装するための手順は、以下で説明する。

#### 4.11.2.3.1. カスタムViewの実装

テキストファイルをダウンロードする例を用いて、説明を行う。

AbstractFileDownloadViewを継承したクラスの実装例

    @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");
        }
    }

項番 | 説明
--- | ---
(1) | 本例では、`@Component`アノテーションを使用して、component-scanの対象としている。 前述した、`org.springframework.web.servlet.view.BeanNameViewResolver`の対象とすることができる。
(2) | `AbstractFileDownloadView`を継承する。
(3) | `getInputStream`メソッドを実装する。 ダウンロード対象の、`InputStream`を返却すること。
(4) | `addResponseHeaderメソッド`を実装する。 ダウンロードするファイルに合わせた、 Content-Dispositionや、ContentTypeを設定する。

#### 4.11.2.3.2. ViewResolverの定義

設定は、PDFファイルをレンダリングする場合と同様である。詳しくは、ViewResolverの定義を参照されたい。

#### 4.11.2.3.3. コントローラでのViewの指定

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

Javaソース

    @GetMapping(value = "download")
    public String download() {
        return "textFileDownloadView"; // (1)
    }

項番 | 説明
--- | ---
(1) | “textFileDownloadView” をメソッドの戻り値として返却することで、 Springのコンテキストで管理された、`TextFileDownloadView`クラスが実行される。

Tip

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

詳細は、Spring の公式ドキュメントSpring Framework Documentation -View Technologies-を参照されたい。