かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その51 )( 貸出申請結果確認画面の作成3 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その50 )( 貸出申請結果確認画面の作成2 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 貸出申請結果確認画面の作成
      • CSVファイルダウンロード処理の実装

参照したサイト・書籍

  1. Java Code Examples for org.dbunit.database.DatabaseConfig
    http://www.programcreek.com/java-api-examples/index.php?api=org.dbunit.database.DatabaseConfig

    • dbUnit の DatabaseConfig クラスを使用して設定する時の実装方法を調査した時に参照しました。
  2. Spring MVC with CSV File Download Example
    http://www.codejava.net/frameworks/spring/spring-mvc-with-csv-file-download-example

  3. indexnext |previous |TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.0.1.RELEASE documentation - 5.18. ファイルダウンロード
    http://terasolunaorg.github.io/guideline/5.0.1.RELEASE/ja/ArchitectureInDetail/FileDownload.html

目次

  1. build タスクを実行するとデータが元に戻らない。。。?
  2. CSVファイルダウンロードの実装方法
  3. 画面に各実装方法用にボタンを作成する
  4. HttpServletResponse に直接出力する方法で実装する
  5. AbstractView クラスのサブクラスを作成する方法で実装する
  6. AbstractView クラスのサブクラスを作成する方法で実装する ( 補足 )
  7. 次回は。。。

手順

build タスクを実行するとデータが元に戻らない。。。?

CSVファイルダウンロードの実装方法を調べていていろいろ試していた時に、build タスクを実行すると貸出承認画面で承認したはずのデータが消えることと、カラムの値が空文字列の時にテストがエラーになることに気づきました。

最初に「build タスクを実行すると貸出承認画面で承認したはずのデータが消える」方の原因を調べたところ、TestDataResource クラスの backupDb メソッドでは空文字列を [null] に変換しているのに、

        replacementDatasetBackup.addReplacementObject("", NULL_STRING);

restoreDb メソッドでは [null] を null に変換しており、変換前と後の値が一致していないことに気づきました。

            replacementDatasetRestore.addReplacementObject(NULL_STRING, null);

backupDb メソッドの方を 空文字列 → null に修正したところ、現象が出なくなりました。

次に「カラムの値が空文字列の時にテストがエラーになる」方の原因を調査したところ、以前 Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その16 )( 検索/一覧画面 ( MyBatis-Spring版 ) 作成3 )dbUnit に cannot insert an empty string の問題があり allowEmptyFields という機能が追加されて 2.5.1 で修正されたらしいが、その時はまだ 2.5.0 までしかリリースされておらず使えなかったと書いていました。build.gradle を見ると今は 2.5.1 を使用しているので allowEmptyFields の機能を有効にして対応します。DatabaseConnection クラスのインスタンスを生成するところを以下のように変更します。

        IDatabaseConnection conn = new DatabaseConnection(dataSource.getConnection());
        DatabaseConfig databaseConfig = conn.getConfig();
        databaseConfig.setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, true);

最終的に src/test/java/ksbysample/common/test/rule/db の下の TestDataResource.javaリンク先の内容 に変更します。

動作確認します。まずは lending_book テーブルのデータを以下の状態にします。

f:id:ksby:20160202004522p:plain

  • lending_book_id = 523 のデータの lending_app_flg だけ空文字列にしています。

clean タスクの実行→「Rebuild Project」メニューの実行→build タスクの実行を行い、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

f:id:ksby:20160202004958p:plain

再度 lending_book テーブルをリロードすると元のデータに戻っていることが確認できます ( なんか大したことがないように見えますが、以前の実装だとこの null と空文字列が混在するデータを元に戻すことができなかったんですよね。。。 )。

f:id:ksby:20160202005233p:plain

CSVファイルダウンロードの実装方法

ここからが本題です。

Spring MVC ではファイルダウンロードを実装する方法として HttpServletResponse に直接出力する方法と、AbstractView クラスのサブクラスを作成する方法の2通りあるようですので、両方試してみることにします。

CSV ファイルを取り扱うライブラリには uniVocity-parses を使用します。

画面に各実装方法用にボタンを2つ設ける

各実装方法用にボタンを作成します。

  1. src/main/java/ksbysample/webapp/lending/web/confirmresult の下の ConfirmresultController.javaリンク先のその1の内容 に変更します。

  2. src/main/resources/templates/confirmresult の下の confirmresult.html を リンク先の内容 に変更します。以下の画面になります。

    f:id:ksby:20160130201518p:plain

HttpServletResponse に直接出力する方法で実装する

  1. src/main/java/ksbysample/webapp/lending/helper の下に download.booklistcsv パッケージを作成します。

  2. CSV のデータを入れるクラスを作成します。src/main/java/ksbysample/webapp/lending/helper/download/booklistcsv の下に BookListCsvData.java を作成します。作成後、リンク先の内容 に変更します。

  3. LendingBook → BookListCsvData への変換用クラスを作成します。src/main/java/ksbysample/webapp/lending/helper/download/booklistcsv の下に BookListCsvDataConverter.java を作成します。作成後、リンク先の内容 に変更します。

  4. src/main/java/ksbysample/webapp/lending/helper/download の下に DataDownloadHelper.java を作成します。作成後、リンク先の内容 に変更します。

  5. src/main/java/ksbysample/webapp/lending/helper/download/booklistcsv の下に BookListCsvDownloadHelper.java を作成します。作成後、リンク先の内容 に変更します。

  6. src/main/java/ksbysample/webapp/lending/web/confirmresult の下の ConfirmresultService.javaリンク先の内容 に変更します。

  7. src/main/java/ksbysample/webapp/lending/web/confirmresult の下の ConfirmresultController.javaリンク先のその2の内容 に変更します。

  8. 動作確認します。データは以下の状態です。

    f:id:ksby:20160203010238p:plain

  9. Gradle projects View から bootRun タスクを実行して Tomcat を起動します。

  10. ブラウザを起動し http://localhost:8080/confirmresult?lendingAppId=105 へアクセスします。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。

    貸出申請結果確認画面が表示されます。

    f:id:ksby:20160203010546p:plain

  11. CSVダウンロード ( HttpServletResponse )」ボタンをクリックします。CSVのデータがダウンロードされることが確認できます。

    f:id:ksby:20160203010812p:plain

  12. Ctrl+F2 を押して Tomcat を停止します。

  13. 一旦 commit します。

AbstractView クラスのサブクラスを作成する方法で実装する

Controller から呼び出す View は文字列で指定します。ViewResolver は BeanNameViewResolver を使用します。

  1. src/main/java/ksbysample/webapp/lending の下に view パッケージを作成します。

  2. src/main/java/ksbysample/webapp/lending/view の下に BookListCsvView.java を作成します。作成後、リンク先の内容 に変更します。

  3. src/main/java/ksbysample/webapp/lending/web/confirmresult の下の ConfirmresultController.javaリンク先のその3の内容 に変更します。

  4. 動作確認します。データは「HttpServletResponse に直接出力する方法で実装する」と同じ状態です。

  5. Gradle projects View から bootRun タスクを実行して Tomcat を起動します。

  6. ブラウザを起動し http://localhost:8080/confirmresult?lendingAppId=105 へアクセスします。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。

    貸出申請結果確認画面が表示されます。

    f:id:ksby:20160205013051p:plain

  7. CSVダウンロード ( AbstractView )」ボタンをクリックします。CSVのデータがダウンロードされることが確認できます。今回は文字コードSJIS ( Windows-31J ) になっています。

    f:id:ksby:20160205013230p:plain

  8. Ctrl+F2 を押して Tomcat を停止します。

  9. 一旦 commit します。

AbstractView クラスのサブクラスを作成する方法で実装する ( 補足 )

上では View を文字列で指定するために BookListCsvView に @Component(value = "BookListCsvView") アノテーションを付加しましたが、次に示す方法でも呼び出すことができます。

まず BookListCsvView クラスには @Component アノテーションを付加しません。

public class BookListCsvView extends AbstractView {

    ..........

Controller クラスから View を呼び出す際には以下のように new 演算子インスタンスを生成します。

    @RequestMapping(value = "/filedownloadByView", method = RequestMethod.POST)
    public ModelAndView filedownloadByView(ConfirmresultForm confirmresultForm
            , BindingResult bindingResult) {
        ..........

        ModelAndView modelAndView = new ModelAndView(new BookListCsvView());

        ..........
    }

この実装でも CSV のデータをダウンロードできます。

次回は。。。

貸出申請したユーザでなければ、共通エラー画面を表示し HTTPステータスコードの 403 を返す処理を実装します。

ソースコード

TestDataResource.java

package ksbysample.common.test.rule.db;

import org.dbunit.DatabaseUnitException;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;

@Component
public class TestDataResource extends TestWatcher {

    private static final String TESTDATA_BASE_DIR = "src/test/resources/testdata/base";
    private static final String BACKUP_FILE_NAME = "ksbylending_backup";
    private static final String NULL_STRING = "[null]";

    @Autowired
    private DataSource dataSource;

    @Autowired
    private TestDataLoader testDataLoader;

    private File backupFile;

    @Override
    protected void starting(Description description) {
        IDatabaseConnection conn = null;
        try {
            // @NouseTestDataResource アノテーションがテストメソッドに付加されていない場合には処理を実行する
            if (!hasNoUseTestDataResourceAnnotation(description)) {
                conn = createDatabaseConnection(dataSource);

                // バックアップを取得する
                backupDb(conn);

                // TESTDATA_BASE_DIR で指定されたディレクトリ内のテストデータをロードする
                testDataLoader.load(TESTDATA_BASE_DIR);

                // テストメソッドに @TestData アノテーションが付加されている場合には、
                // アノテーションで指定されたテストデータをロードする
                loadTestData(description);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (conn != null) conn.close();
            } catch (Exception ignored) {}
        }
    }

    @Override
    protected void finished(Description description) {
        IDatabaseConnection conn = null;
        try {
            // @NouseTestDataResource アノテーションがテストメソッドに付加されていない場合には処理を実行する
            if (!hasNoUseTestDataResourceAnnotation(description)) {
                conn = createDatabaseConnection(dataSource);

                // バックアップからリストアする
                restoreDb(conn);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (conn != null) conn.close();
            } catch (Exception ignored) {}

            if (backupFile != null) {
                try {
                    Files.delete(backupFile.toPath());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                backupFile = null;
            }
        }
    }

    private IDatabaseConnection createDatabaseConnection(DataSource dataSource) throws SQLException, DatabaseUnitException {
        IDatabaseConnection conn = new DatabaseConnection(dataSource.getConnection());
        DatabaseConfig databaseConfig = conn.getConfig();
        databaseConfig.setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, true);
        return conn;
    }

    private boolean hasNoUseTestDataResourceAnnotation(Description description) {
        Collection<Annotation> annotationList = description.getAnnotations();
        boolean result = annotationList.stream()
                .anyMatch(annotation -> annotation instanceof NoUseTestDataResource);
        return result;
    }

    private void backupDb(IDatabaseConnection conn)
            throws DataSetException, IOException {
        QueryDataSet partialDataSet = new QueryDataSet(conn);

        // TESTDATA_BASE_DIR で指定されたディレクトリ内の table-ordering.txt に記述されたテーブル名一覧を取得し、
        // バックアップテーブルとしてセットする
        List<String> backupTableList = Files.readAllLines(Paths.get(TESTDATA_BASE_DIR, "table-ordering.txt"));
        for (String backupTable :  backupTableList) {
            partialDataSet.addTable(backupTable);
        }

        ReplacementDataSet replacementDatasetBackup = new ReplacementDataSet(partialDataSet);
        replacementDatasetBackup.addReplacementObject(null, NULL_STRING);
        this.backupFile = File.createTempFile(BACKUP_FILE_NAME, "xml");
        try (FileOutputStream fos = new FileOutputStream(this.backupFile)) {
            FlatXmlDataSet.write(replacementDatasetBackup, fos);
        }
    }

    private void restoreDb(IDatabaseConnection conn)
            throws MalformedURLException, DatabaseUnitException, SQLException {
        if (this.backupFile != null) {
            IDataSet dataSet = new FlatXmlDataSetBuilder().build(this.backupFile);
            ReplacementDataSet replacementDatasetRestore = new ReplacementDataSet(dataSet);
            replacementDatasetRestore.addReplacementObject(NULL_STRING, null);
            DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDatasetRestore);
        }
    }

    private void loadTestData(Description description) {
        description.getAnnotations().stream()
                .filter(annotation -> annotation instanceof TestData)
                .forEach(annotation -> {
                    TestData testData = (TestData)annotation;
                    testDataLoader.load(testData.value());
                });
    }

}
  • createDatabaseConnection メソッドを追加し、starting 及び finished メソッドIDatabaseConnection conn の実体クラスを生成する時に createDatabaseConnection メソッドを呼び出すように変更します。
  • backupDb メソッド内の実装を replacementDatasetBackup.addReplacementObject("", NULL_STRING);replacementDatasetBackup.addReplacementObject(null, NULL_STRING); へ変更します。

ConfirmresultController.java

■その1

package ksbysample.webapp.lending.web.confirmresult;

import ksbysample.webapp.lending.exception.WebApplicationRuntimeException;
import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper;
import ksbysample.webapp.lending.web.lendingapproval.LendingapprovalForm;
import ksbysample.webapp.lending.web.lendingapproval.LendingapprovalParamForm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/confirmresult")
public class ConfirmresultController {

    @Autowired
    private MessagesPropertiesHelper messagesPropertiesHelper;

    @Autowired
    private ConfirmresultService confirmresultService;
    
    @RequestMapping
    public String index(@Validated ConfirmresultParamForm confirmresultParamForm
            , BindingResult bindingResult
            , ConfirmresultForm confirmresultForm
            , BindingResult bindingResultOfConfirmresultForm) {
        if (bindingResult.hasErrors()) {
            throw new WebApplicationRuntimeException(
                    messagesPropertiesHelper.getMessage("ConfirmresultParamForm.lendingAppId.emptyerr", null));
        }

        // 画面に表示するデータを取得する
        confirmresultService.setDispData(confirmresultParamForm.getLendingAppId(), confirmresultForm);

        // 指定された貸出申請IDで承認済のデータがない場合には、貸出申請結果確認画面上にエラーメッセージを表示する
        if (confirmresultForm.getLendingApp() == null) {
            bindingResultOfConfirmresultForm.reject("ConfirmresultForm.lendingApp.nodataerr");
        }

        return "confirmresult/confirmresult";
    }

    @RequestMapping(value = "/filedownloadByResponse", method = RequestMethod.POST)
    public String filedownloadByResponse(ConfirmresultForm confirmresultForm
            , BindingResult bindingResult) {
        return "confirmresult/confirmresult";
    }

    @RequestMapping(value = "/filedownloadByView", method = RequestMethod.POST)
    public String filedownloadByView(ConfirmresultForm confirmresultForm
            , BindingResult bindingResult) {
        return "confirmresult/confirmresult";
    }
    
}

■その2

    @RequestMapping(value = "/filedownloadByResponse", method = RequestMethod.POST)
    public void filedownloadByResponse(@Validated ConfirmresultForm confirmresultForm
            , BindingResult bindingResult
            , HttpServletResponse response) throws IOException {
        if (bindingResult.hasErrors()) {
            throw new WebApplicationRuntimeException(
                    messagesPropertiesHelper.getMessage("ConfirmresultParamForm.lendingAppId.emptyerr", null));
        }

        // データを取得する
        List<BookListCsvData> bookListCsvDataList
                = confirmresultService.getDownloadData(confirmresultForm.getLendingApp().getLendingAppId());

        // response に CSVデータを出力する
        DataDownloadHelper dataDownloadHelper
                = new BookListCsvDownloadHelper(confirmresultForm.getLendingApp().getLendingAppId(), bookListCsvDataList);
        dataDownloadHelper.setFileNameToResponse(response);
        dataDownloadHelper.writeDataToResponse(response);
    }

■その3

    @RequestMapping(value = "/filedownloadByView", method = RequestMethod.POST)
    public ModelAndView filedownloadByView(ConfirmresultForm confirmresultForm
            , BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            throw new WebApplicationRuntimeException(
                    messagesPropertiesHelper.getMessage("ConfirmresultParamForm.lendingAppId.emptyerr", null));
        }

        // データを取得する
        List<BookListCsvData> bookListCsvDataList
                = confirmresultService.getDownloadData(confirmresultForm.getLendingApp().getLendingAppId());

        ModelAndView modelAndView = new ModelAndView("BookListCsvView");
        modelAndView.addObject("lendingAppId", confirmresultForm.getLendingApp().getLendingAppId());
        modelAndView.addObject("bookListCsvDataList", bookListCsvDataList);

        return modelAndView;
    }
  • filedownloadByView メソッドを実装します。ポイントは以下の点です。
    • 戻り値の型を StringModelAndView へ変更します。
    • ModelAndView modelAndView = new ModelAndView("BookListCsvView"); のように ModelAndView クラスのインスタンスを生成する際に View を Bean 名の文字列 "BookListCsvView" で指定します。

confirmresult.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <title>貸出申請結果確認</title>
    <!-- Tell the browser to be responsive to screen width -->
    <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/>
    <link th:replace="common/head-cssjs"/>

    <style>
        .content-wrapper {
            background-color: #fffafa;
        }
        .selected-library {
            color: #ffffff !important;
            font-size: 100%;
            font-weight: 700;
        }
        .buttom-btn-area {
            padding-top: 10px;
        }
        .box-body.no-padding {
            padding-bottom: 10px !important;
        }
        .table>tbody>tr>td
        , .table>tbody>tr>th
        , .table>tfoot>tr>td
        , .table>tfoot>tr>th
        , .table>thead>tr>td
        , .table>thead>tr>th {
            padding: 5px;
            font-size: 90%;
        }
        .jp-gothic {
            font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
        }
    </style>
</head>

<!-- ADD THE CLASS layout-top-nav TO REMOVE THE SIDEBAR. -->
<body class="skin-blue layout-top-nav">
<div class="wrapper">

    <!-- Main Header -->
    <div th:replace="common/mainparts :: main-header"></div>

    <!-- Full Width Column -->
    <div class="content-wrapper">
        <div class="container">
            <!-- Content Header (Page header) -->
            <section class="content-header">
                <h1>貸出申請結果確認</h1>
            </section>

            <!-- Main content -->
            <section class="content">
                <div class="row">
                    <div class="col-xs-12">
                        <form id="confirmresultForm" method="post" action="/confirmresult/filedownload" 
                              th:action="@{/confirmresult/filedownload}" th:object="${confirmresultForm}">
                            <div th:replace="common/mainparts :: alert-danger"></div>
                            <div th:replace="common/mainparts :: alert-success"></div>
                            <div class="box" th:if="*{lendingApp != null}">
                                <div class="box-body no-padding">
                                    <div class="col-xs-6 no-padding">
                                        <table class="table table-bordered">
                                            <colgroup>
                                                <col width="30%"/>
                                                <col width="70%"/>
                                            </colgroup>
                                            <tr>
                                                <th class="bg-purple">貸出申請ID</th>
                                                <td th:text="*{lendingApp.lendingAppId}">1</td>
                                            </tr>
                                            <tr>
                                                <th class="bg-purple">ステータス</th>
                                                <td th:text="${@vh.getText('LendingAppStatusValues', confirmresultForm.lendingApp.status)}">承認済</td>
                                            </tr>
                                            <tr>
                                                <th class="bg-purple">申請者</th>
                                                <td th:text="*{lendingUserName}">田中 太郎</td>
                                            </tr>
                                            <tr>
                                                <th class="bg-purple">承認者</th>
                                                <td th:text="*{approvalUserName}">鈴木 花子</td>
                                            </tr>
                                        </table>
                                        <input type="hidden" th:field="*{lendingApp.lendingAppId}"/>
                                    </div>
                                    <br/>

                                    <table class="table">
                                        <colgroup>
                                            <col width="5%"/>
                                            <col width="20%"/>
                                            <col width="20%"/>
                                            <col width="20%"/>
                                            <col width="15%"/>
                                            <col width="20%"/>
                                        </colgroup>
                                        <thead class="bg-purple">
                                        <tr>
                                            <th>No.</th>
                                            <th>ISBN</th>
                                            <th>書名</th>
                                            <th>申請理由</th>
                                            <th>承認/却下</th>
                                            <th>却下理由</th>
                                        </tr>
                                        </thead>
                                        <tbody class="jp-gothic">
                                        <tr th:each="approvedBookForm, iterStat : *{approvedBookFormList}">
                                            <td th:text="${iterStat.count}">1</td>
                                            <td th:text="${approvedBookForm.isbn}">978-4-7741-6366-6</td>
                                            <td th:text="${approvedBookForm.bookName}">GitHub実践入門</td>
                                            <td th:text="${approvedBookForm.lendingAppReason}">開発で使用する為</td>
                                            <td th:text="${@vh.getText('LendingBookApprovalResultValues', approvedBookForm.approvalResult)}">承認</td>
                                            <td th:text="${approvedBookForm.approvalReason}"></td>
                                        </tr>
                                        </tbody>
                                    </table>
                                    <div class="buttom-btn-area text-center">
                                        <button class="btn bg-blue js-btn-filedownload-response"><i class="fa fa-download"></i> CSVダウンロード ( HttpServletResponse )</button>
                                        <button class="btn bg-orange js-btn-filedownload-view"><i class="fa fa-download"></i> CSVダウンロード ( AbstractView )</button>
                                    </div>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </section>
            <!-- /.content -->
        </div>
        <!-- /.container -->
    </div>

</div>
<!-- ./wrapper -->

<script th:replace="common/bottom-js"></script>
<script type="text/javascript">
    <!--
    $(document).ready(function() {
        $(".js-btn-filedownload-response").click(function(){
            $("#confirmresultForm").attr("action", "/confirmresult/filedownloadByResponse");
            $("#confirmresultForm").submit();
            return false;
        });
        $(".js-btn-filedownload-view").click(function(){
            $("#confirmresultForm").attr("action", "/confirmresult/filedownloadByView");
            $("#confirmresultForm").submit();
            return false;
        });
    });
    -->
</script>
</body>
</html>
  • <button class="btn bg-blue js-btn-filedownload"><i class="fa fa-download"></i> CSVダウンロード</button> を以下の2行に変更します。
    • <button class="btn bg-blue js-btn-filedownload-response"><i class="fa fa-download"></i> CSVダウンロード ( HttpServletResponse )</button>
    • <button class="btn bg-orange js-btn-filedownload-view"><i class="fa fa-download"></i> CSVダウンロード ( AbstractView )</button>
  • Javascript で js-btn-filedownload-response, js-btn-filedownload-view click時の処理を実装します。

BookListCsvData.java

package ksbysample.webapp.lending.helper.download.booklistcsv;

import com.univocity.parsers.annotations.Parsed;
import lombok.Data;

@Data
public class BookListCsvData {

    @Parsed(field = "ISBN")
    private String isbn;

    @Parsed(field = "書名")
    private String bookName;

    @Parsed(field = "申請理由")
    private String lendingAppReason;

    @Parsed(field = "承認/却下")
    private String approvalResultStr;

    @Parsed(field = "却下理由")
    private String approvalReason;

}

BookListCsvDataConverter.java

package ksbysample.webapp.lending.helper.download.booklistcsv;

import ksbysample.webapp.lending.entity.LendingBook;
import ksbysample.webapp.lending.values.LendingBookApprovalResultValues;
import ksbysample.webapp.lending.values.ValuesHelper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.stream.Collectors;

@Component
public class BookListCsvDataConverter {

    @Autowired
    private ValuesHelper vh;

    public List<BookListCsvData> convertFrom(List<LendingBook> lendingBookList) {
        List<BookListCsvData> bookListCsvDataList = null;
        if (lendingBookList != null) {
            bookListCsvDataList = lendingBookList.stream()
                    .map(lendingBook -> {
                        BookListCsvData bookListCsvData = new BookListCsvData();
                        BeanUtils.copyProperties(lendingBook, bookListCsvData);
                        bookListCsvData.setApprovalResultStr(
                                vh.getText(LendingBookApprovalResultValues.class
                                        , lendingBook.getApprovalResult()));
                        return bookListCsvData;
                    })
                    .collect(Collectors.toList());
        }
        return bookListCsvDataList;
    }

}

DataDownloadHelper.java

package ksbysample.webapp.lending.helper.download;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public interface DataDownloadHelper {

    public void setFileNameToResponse(HttpServletResponse response);

    public void writeDataToResponse(HttpServletResponse response) throws IOException;

}

BookListCsvDownloadHelper.java

package ksbysample.webapp.lending.helper.download.booklistcsv;

import com.univocity.parsers.common.processor.BeanWriterProcessor;
import com.univocity.parsers.csv.CsvWriter;
import com.univocity.parsers.csv.CsvWriterSettings;
import ksbysample.webapp.lending.helper.download.DataDownloadHelper;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

public class BookListCsvDownloadHelper implements DataDownloadHelper {

    private static final String[] CSV_HEADER = new String[]{"ISBN", "書名", "申請理由", "承認/却下", "却下理由"};
    private static final String CSV_FILE_NAME_FORMAT = "booklist-%s.csv";

    private Long lendingAppId;

    private List<BookListCsvData> bookListCsvDataList;

    public BookListCsvDownloadHelper(Long lendingAppId, List<BookListCsvData> bookListCsvDataList) {
        this.lendingAppId = lendingAppId;
        this.bookListCsvDataList = bookListCsvDataList;
    }

    public String getCsvFileName() {
        return String.format(CSV_FILE_NAME_FORMAT, this.lendingAppId);
    }

    @Override
    public void setFileNameToResponse(HttpServletResponse response) {
        response.setHeader("Content-Disposition", String.format("attachment; filename=\"%s\"", getCsvFileName()));
    }

    @Override
    public void writeDataToResponse(HttpServletResponse response) throws IOException {
        CsvWriterSettings settings = new CsvWriterSettings();
        settings.setHeaders(CSV_HEADER);
        BeanWriterProcessor<BookListCsvData> writerProcessor = new BeanWriterProcessor<>(BookListCsvData.class);
        settings.setRowWriterProcessor(writerProcessor);

        CsvWriter writer = new CsvWriter(response.getWriter(), settings);
        writer.writeHeaders();
        writer.processRecordsAndClose(bookListCsvDataList);
    }

}

ConfirmresultService.java

package ksbysample.webapp.lending.web.confirmresult;

import ksbysample.webapp.lending.dao.LendingAppDao;
import ksbysample.webapp.lending.dao.LendingBookDao;
import ksbysample.webapp.lending.dao.UserInfoDao;
import ksbysample.webapp.lending.entity.LendingApp;
import ksbysample.webapp.lending.entity.LendingBook;
import ksbysample.webapp.lending.entity.UserInfo;
import ksbysample.webapp.lending.helper.download.booklistcsv.BookListCsvData;
import ksbysample.webapp.lending.helper.download.booklistcsv.BookListCsvDataConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

import static ksbysample.webapp.lending.values.LendingAppStatusValues.APPLOVED;
import static ksbysample.webapp.lending.values.LendingBookLendingAppFlgValues.APPLY;

@Service
public class ConfirmresultService {

    @Autowired
    private LendingAppDao lendingAppDao;

    @Autowired
    private UserInfoDao userInfoDao;

    @Autowired
    private LendingBookDao lendingBookDao;

    @Autowired
    private BookListCsvDataConverter bookListCsvDataConverter;

    public void setDispData(Long lendingAppId, ConfirmresultForm confirmresultForm) {
        LendingApp lendingApp = lendingAppDao.selectByIdAndStatus(lendingAppId, Arrays.asList(APPLOVED.getValue()));
        String lendingUserName = "";
        String approvalUserName = "";
        if (lendingApp != null) {
            UserInfo lendingUserInfo = userInfoDao.selectById(lendingApp.getLendingUserId());
            lendingUserName = lendingUserInfo.getUsername();
            UserInfo approvalUserInfo = userInfoDao.selectById(lendingApp.getApprovalUserId());
            approvalUserName = approvalUserInfo.getUsername();
        }
        List<LendingBook> lendingBookList
                = lendingBookDao.selectByLendingAppIdAndLendingAppFlg(lendingAppId, APPLY.getValue());

        confirmresultForm.setLendingApp(lendingApp);
        confirmresultForm.setLendingUserName(lendingUserName);
        confirmresultForm.setApprovalUserName(approvalUserName);
        confirmresultForm.setApprovedBookFormListFromLendingBookList(lendingBookList);
    }

    public List<BookListCsvData> getDownloadData(Long lendingAppId) {
        List<LendingBook> lendingBookList
                = lendingBookDao.selectByLendingAppIdAndLendingAppFlg(lendingAppId, APPLY.getValue());
        List<BookListCsvData> bookListCsvDataList = bookListCsvDataConverter.convertFrom(lendingBookList);
        return bookListCsvDataList;
    }

}

BookListCsvView.java

package ksbysample.webapp.lending.view;

import com.univocity.parsers.common.processor.BeanWriterProcessor;
import com.univocity.parsers.csv.CsvWriter;
import com.univocity.parsers.csv.CsvWriterSettings;
import ksbysample.webapp.lending.helper.download.booklistcsv.BookListCsvData;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.AbstractView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;

@Component(value = "BookListCsvView")
public class BookListCsvView extends AbstractView {

    private static final String[] CSV_HEADER = new String[]{"ISBN", "書名", "申請理由", "承認/却下", "却下理由"};
    private static final String CSV_FILE_NAME_FORMAT = "booklist-%s.csv";

    @Override
    protected void renderMergedOutputModel(Map<String, Object> model
            , HttpServletRequest request, HttpServletResponse response) throws Exception {
        Long lendingAppId = (Long) model.get("lendingAppId");
        List<BookListCsvData> bookListCsvDataList = (List<BookListCsvData>) model.get("bookListCsvDataList");

        response.setContentType("application/octet-stream; charset=Windows-31J;");
        response.setHeader("Content-Disposition"
                , String.format("attachment; filename=\"%s\"", String.format(CSV_FILE_NAME_FORMAT, lendingAppId)));

        CsvWriterSettings settings = new CsvWriterSettings();
        settings.setHeaders(CSV_HEADER);
        BeanWriterProcessor<BookListCsvData> writerProcessor = new BeanWriterProcessor<>(BookListCsvData.class);
        settings.setRowWriterProcessor(writerProcessor);

        CsvWriter writer = new CsvWriter(response.getWriter(), settings);
        writer.writeHeaders();
        writer.processRecordsAndClose(bookListCsvDataList);
    }

}
  • AbstractView クラスを継承して View クラスを作成します。この時 Controller クラスから "BookListCsvView" の文字列 ( Bean名 ) で呼び出せるよう @Component(value = "BookListCsvView") アノテーションを付加します。value 属性で Bean 名を明示する必要があります。
  • BookListCsvView ではダウンロードする CSV ファイルの文字コードWindows-31J にするために response.setContentType("application/octet-stream; charset=Windows-31J;"); を呼び出しています。

履歴

2016/02/05
初版発行。