かんがるーさんの日記

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

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その23 )( 貸出希望書籍 CSV ファイルアップロード画面の作成2 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その22 )( 貸出希望書籍 CSV ファイルアップロード画面の作成 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 貸出希望書籍 CSV ファイルアップロード画面の作成
      • CSVファイルアップロード機能を作成します。

参照したサイト・書籍

  1. Uploading Files
    https://spring.io/guides/gs/uploading-files/

    • Spring の公式サイトのファイルアップロードの実装方法に関する記事です。
  2. uniVocity/univocity-parsers
    https://github.com/uniVocity/univocity-parsers

    • JavaCSV Parser ライブラリ uniVocity-parsers の GitHub のページです。今回は CSV ファイルの読み込みにこのライブラリを使用します。
  3. ABOUT UNIVOCITY-PARSERS
    http://www.univocity.com/pages/about-parsers

    • uniVocity-parses の公式サイトのトップページです。
  4. UNIVOCITY-PARSERS DOCUMENTATION
    http://www.univocity.com/pages/parsers-documentation

    • uniVocity-parses の公式サイトの Documentation ページです。ここに Tutorials や Javadocs へのリンクがあります。
  5. Tutorial: Using Thymeleaf - 6.2 Keeping iteration status
    http://www.thymeleaf.org/doc/tutorials/2.1/usingthymeleaf.html#keeping-iteration-status

    • Thymeleaf の th:each でループ処理中のデータの index ( 何件目のデータか ) や、ループ処理対象のリストの件数を取得する方法が記載されています。今回は画面上の表の No. カラムに 1 から順にインクリメントした数字を表示するために使用しています。

目次

  1. CSVファイルアップロード機能の作成
    1. テスト用CSVファイルを用意する
    2. UploadBooklistForm クラスを作成し、ファイルをアップロードできるようにする
    3. アップロードされたファイルが保存される一時ディレクトリはどこなのか?
    4. uniVocity-parsers を利用可能にする
    5. UploadBooklistFormValidator クラスを作成し、アップロードされたファイルをチェックする
    6. アップロードされたCSVファイルのデータを DB に保存する
    7. 次の画面に遷移しアップロードされたCSVファイルのデータを表示する
    8. 動作確認
  2. 次回は。。。

手順

CSVファイルアップロード機能の作成

テスト用CSVファイルを用意する

  1. アップロードする CSV ファイルは以下の仕様とします。

    • 文字コードWindows-31J
    • 改行コードは CR+LF。
    • 項目は必ずダブルクォーテーションで囲む。
    • 項目はISBN、書名の2項目固定。
    • 1行目はヘッダ行とし、"ISBN","書名" と記述する。
  2. デスクトップに「テスト.csv」というファイルを作成します。作成後、リンク先の内容 に変更します。

UploadBooklistForm クラスを作成し、ファイルをアップロードできるようにする

  1. アップロードファイルの最大サイズを設定しようと思い、src/main/resources の下の application.properties を開いて "multipart." を入力して候補を表示させてみたところ、以下の画像の内容が表示されました。

    f:id:ksby:20150923205556p:plain

    multipart.max-file-size の設定は 1MB になっており、この値で十分なので特に設定は行わないことにします。

    ※multipart の設定項目については Appendix A. Common application properties に記載されています。IntelliJ IDEA の候補では multipart.enabled の設定値が false で表示されていますが、Appendix A を見るとデフォルト値は true でしたのでこちらも特に設定はしません。

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

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

  4. これでファイルをアップロードするとコンソールにファイルの内容が出力されるようになります。動作確認してみます。

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

  6. ブラウザから http://localhost:8080/booklist/ にアクセスして テスト.csv をアップロードすると、コンソールにファイルの内容が出力されることが確認できます。

    f:id:ksby:20150924002100p:plain

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

アップロードされたファイルが保存される一時ディレクトリはどこなのか?

application.properties で server.tomcat.basedir でディレクトリが設定されている場合にはそのディレクトリの下の work\Tomcat\localhost\ROOT が、設定されていない場合には Javaシステムプロパティ java.io.tmpdir に設定されているディレクトリ ( Windows の場合には環境変数 TEMP に設定されているディレクトリになります ) の下の tomcat.~\work\Tomcat\localhost\ROOT の下が一時ディレクトリとして使用されます。

ksbysample-webapp-lending の場合、develop 環境では server.tomcat.basedir を設定していませんので環境変数 TEMP に設定されているディレクトリの下が一時ディレクトリになります。具体例を示すと C:\Users\root\AppData\Local\Temp\tomcat.6243487550496604198.8080\work\Tomcat\localhost\ROOT です。

以下の手順で確認しました。

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

  2. 環境変数 TEMP に設定されているディレクトリ ( 例 C:\Users\root\AppData\Local\Temp ) の下のディレクトリ・ファイルの内、削除可能なものを全て削除します。

  3. ブラウザから http://localhost:8080/booklist/ にアクセスしてファイルをアップロードするとエラーになります。

  4. ログを見ると Caused by: java.io.IOException: The temporary upload location [C:\Users\root\AppData\Local\Temp\tomcat.6243487550496604198.8080\work\Tomcat\localhost\ROOT] is not valid のログが出力されることが確認できます。

product 環境の場合には application-product.properties に server.tomcat.basedir=C:/webapps/ksbysample-webapp-lending と設定していますので、C:\webapps\ksbysample-webapp-lending\work\Tomcat\localhost\ROOT がアップロードファイルの一時ディレクトリになります。

uniVocity-parsers を利用可能にする

CSV ファイルの読み込みには uniVocity-parsers を利用します。

  1. build.gradle を リンク先の内容 に変更します。

  2. Gradle projects View の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

UploadBooklistFormValidator クラスを作成し、アップロードされたファイルをチェックする

  1. Web アプリケーションで汎用的に使用する RuntimeException の継承クラスを作成します。src/main/java/ksbysample/webapp/lending の下に exception パッケージを作成します。

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

  3. messages_ja_JP.properties からメッセージを取得する Helper クラスを作成します。src/main/java/ksbysample/webapp/lending/helper の下に message パッケージを作成します。

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

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

  6. src/main/java/ksbysample/webapp/lending/service/file の下に BooklistCsvFileService.java を作成します。作成後、リンク先のその1の内容 に変更します。

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

  8. src/main/resources の下の messages_ja_JP.properties を リンク先の内容 に変更します。

  9. src/main/resources/templates/booklist の下の booklist.html を リンク先の内容 に変更します。

  10. 動作確認してみます。Gradle projects View から bootRun タスクを実行して Tomcat を起動します。

  11. エラーになる CSV ファイルを用意します。デスクトップにテスト.csv をコピーして「テスト2.csv」というファイルを作成します。作成後、リンク先の内容 に変更します。

  12. ブラウザから http://localhost:8080/booklist/ にアクセスして テスト2.csv をアップロードすると、以下のエラーメッセージが表示されることが確認できます。

    f:id:ksby:20150926151459p:plain

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

アップロードされたCSVファイルのデータを DB に保存する

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

  2. src/main/java/ksbysample/webapp/lending/service/file の下の BooklistCsvFileService.javaリンク先のその2の内容 に変更します。

  3. lending_app テーブルの status にセットする値を定義する Enum を作成します。src/main/java/ksbysample/webapp/lending の下に values パッケージを作成します。

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

  5. lending_app テーブルの lending_user_id に user_info テーブルの user_id を保存するのですが、ログインしているユーザの user_info.user_id を取得するための実装が漏れていたので追加します。src/main/java/ksbysample/webapp/lending/security の下の LendingUser.javaリンク先の内容 に変更します。

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

  7. lending_app テーブルの approval_user_id に not null 制約が付いているのですが承認するまでは空なので not null 制約を外します。/sql の下の create_table.sqlリンク先の内容 に変更します。

  8. 以下のコマンドを実行し lending_app テーブルを作り直します。

    > psql -U ksbylending_user ksbylending
    ユーザ ksbylending_user のパスワード:
    psql> drop table lending_app cascade;
    psql> create table lending_app (...);
    psql> create table lending_book (...);

    • create table 文は create_table.sql からコピー&ペーストして実行してください。
    • lending_app テーブルを削除する時には外部キー制約のある lending_book テーブルも削除しますので、lending_book テーブルも作成し直しています。
  9. src/main/java/ksbysample/webapp/lending/web/booklist の下に RegisterBooklistForm.java を作成します。作成後、リンク先の内容 に変更します。

  10. src/main/java/ksbysample/webapp/lending/dao の下の LendingBookDao.javaリンク先の内容 に変更します。

  11. src/main/resources/META-INF/ksbysample/webapp/lending/dao/LendingBookDao の下に selectByLendingAppId.sql を作成します。作成後、リンク先の内容 に変更します。

  12. src/main/java/ksbysample/webapp/lending/web/booklist の下に BooklistService.java を作成します。作成後、リンク先のその1の内容 に変更します。

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

次の画面に遷移しアップロードされたCSVファイルのデータを表示する

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

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

  3. src/main/resources/templates/booklist の下の fileupload.html を リンク先の内容 に変更します。

動作確認

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

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

  3. テスト.csv をアップロードします。

    f:id:ksby:20150927103216p:plain

    次の画面にアップロードしたファイルの内容が表示されます。

    f:id:ksby:20150927103800p:plain

  4. lending_app、lending_book テーブルにもデータが登録されていることが確認できます。

    f:id:ksby:20150927104026p:plain

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

  6. 一旦 commit します。

次回は。。。

Spring Boot の 1.2.6 がリリースされていたり、IntelliJ IDEA がバージョンアップしていたりするので、まずはバージョンアップの作業をします。

その後で登録機能を作成します。

ソースコード

テスト.csv

"ISBN","書名"
"978-4-7741-6366-6","GitHub実践入門"
"978-4-7741-5377-3","JUnit実践入門"
"978-4-7973-8014-9","Java最強リファレンス"
"978-4-7973-4778-4","アジャイルソフトウェア開発の奥義"
"978-4-87311-704-1","Javaによる関数型プログラミング"

UploadBooklistForm.java

package ksbysample.webapp.lending.web.booklist;

import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

@Data
public class UploadBooklistForm {

    private MultipartFile fileupload;

}

BooklistController.java

■その1

package ksbysample.webapp.lending.web.booklist;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

@Controller
@RequestMapping("/booklist")
public class BooklistController {

    @RequestMapping
    public String index() {
        return "booklist/booklist";
    }

    @RequestMapping("/fileupload")
    public String fileupload(@Validated UploadBooklistForm uploadBooklistForm
            , BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "booklist/booklist";
        }

        // アップロードされたファイルの内容を出力してみる
        try (
                InputStream is = uploadBooklistForm.getFileupload().getInputStream();
                InputStreamReader isr = new InputStreamReader(is, "Windows-31J");
                BufferedReader br = new BufferedReader(isr);
        ) {
            br.lines()
                    .forEach(System.out::println);
        }
        catch (IOException e) {}

        return "booklist/fileupload";
    }

    @RequestMapping("/register")
    public String register() {
        return "redirect:/booklist/complete";
    }

    @RequestMapping("/complete")
    public String complete() {
        return "booklist/complete";
    }

}
  • fileupload メソッドを上記の内容に変更します。

■その2

package ksbysample.webapp.lending.web.booklist;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/booklist")
public class BooklistController {

    @Autowired
    private UploadBooklistFormValidator uploadBooklistFormValidator;

    @Autowired
    private BooklistService booklistService;
    
    @InitBinder("uploadBooklistForm")
    public void initBinder(WebDataBinder binder) {
        binder.addValidators(uploadBooklistFormValidator);
    }    

    @RequestMapping
    public String index(UploadBooklistForm uploadBooklistForm) {
        return "booklist/booklist";
    }
    
    @RequestMapping("/fileupload")
    public String fileupload(@Validated UploadBooklistForm uploadBooklistForm
            , BindingResult bindingResult
            , Model model) {
        if (bindingResult.hasErrors()) {
            return "booklist/booklist";
        }

        // アップロードされたCSVファイルのデータをDBに保存する
        Long lendingAppId = booklistService.temporarySaveBookListCsvFile(uploadBooklistForm);
        
        return "booklist/fileupload";
    }

    @RequestMapping("/register")
    public String register() {
        return "redirect:/booklist/complete";
    }

    @RequestMapping("/complete")
    public String complete() {
        return "booklist/complete";
    }

}
  • fileupload メソッドの以下の点を変更します。
    • アップロードされたファイルの内容を出力する処理を削除します。
    • Long lendingAppId = booklistService.temporarySaveBookListCsvFile(uploadBooklistForm); を追加します。

■その3

package ksbysample.webapp.lending.web.booklist;

import ksbysample.webapp.lending.entity.LendingBook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@Controller
@RequestMapping("/booklist")
public class BooklistController {

    @Autowired
    private UploadBooklistFormValidator uploadBooklistFormValidator;

    @Autowired
    private BooklistService booklistService;
    
    @InitBinder("uploadBooklistForm")
    public void initBinder(WebDataBinder binder) {
        binder.addValidators(uploadBooklistFormValidator);
    }    

    @RequestMapping
    public String index(UploadBooklistForm uploadBooklistForm) {
        return "booklist/booklist";
    }
    
    @RequestMapping("/fileupload")
    public String fileupload(@Validated UploadBooklistForm uploadBooklistForm
            , BindingResult bindingResult
            , Model model) {
        if (bindingResult.hasErrors()) {
            return "booklist/booklist";
        }

        // アップロードされたCSVファイルのデータをDBに保存する
        Long lendingAppId = booklistService.temporarySaveBookListCsvFile(uploadBooklistForm);

        // 確認画面に表示するデータを取得する
        List<LendingBook> lendingBookList = booklistService.getLendingBookList(lendingAppId);
        RegisterBooklistForm registerBooklistForm = new RegisterBooklistForm(lendingBookList, lendingAppId);
        model.addAttribute("registerBooklistForm", registerBooklistForm);
        
        return "booklist/fileupload";
    }

    @RequestMapping("/register")
    public String register() {
        return "redirect:/booklist/complete";
    }

    @RequestMapping("/complete")
    public String complete() {
        return "booklist/complete";
    }

}
  • fileupload メソッドの以下の点を変更します。
    • 引数に Model model を追加します。
    • 確認画面に表示するデータを取得する処理を追加します。

build.gradle

dependencies {
    def jdbcDriver = "org.postgresql:postgresql:9.4-1201-jdbc41"

    // spring-boot-gradle-plugin によりバージョン番号が自動で設定されるもの
    // Appendix E. Dependency versions ( http://docs.spring.io/spring-boot/docs/current/reference/html/appendix-dependency-versions.html ) 参照
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity3")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.boot:spring-boot-starter-velocity")
    compile("org.springframework.boot:spring-boot-starter-mail")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.springframework.boot:spring-boot-starter-redis")
    compile("org.codehaus.janino:janino")
    testCompile("org.springframework.boot:spring-boot-starter-test")
    // (ここから) gradle でテストを実行した場合に spring-security-test-4.0.1.RELEASE.jar しか classpath に指定されず
    // テストが失敗したため、3.2.7.RELEASE を明記している
    testCompile("org.springframework.security:spring-security-core:3.2.7.RELEASE")
    testCompile("org.springframework.security:spring-security-web:3.2.7.RELEASE")
    // (ここまで) ------------------------------------------------------------------------------------------------------
    testCompile("org.springframework.security:spring-security-test:4.0.1.RELEASE")
    testCompile("org.yaml:snakeyaml")

    // spring-boot-gradle-plugin によりバージョン番号が自動で設定されないもの
    compile("${jdbcDriver}")
    compile("org.seasar.doma:doma:2.3.1")
    compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")
    compile("org.apache.commons:commons-lang3:3.4")
    compile("org.projectlombok:lombok:1.16.4")
    compile("com.google.guava:guava:18.0")
    compile("org.springframework.session:spring-session:1.0.1.RELEASE")
    compile("org.simpleframework:simple-xml:2.7.1")
    compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.6.1")
    compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.5.3")
    compile("com.univocity:univocity-parsers:1.5.6")
    testCompile("org.dbunit:dbunit:2.5.1")
    testCompile("com.icegreen:greenmail:1.4.1")
    testCompile("org.assertj:assertj-core:3.1.0")
    testCompile("com.jayway.jsonpath:json-path:2.0.0")
    testCompile("org.jmockit:jmockit:1.19")

    // for Doma-Gen
    domaGenRuntime("org.seasar.doma:doma-gen:2.3.1")
    domaGenRuntime("${jdbcDriver}")
}
  • compile("com.univocity:univocity-parsers:1.5.6") を追加します。

WebApplicationRuntimeException.java

package ksbysample.webapp.lending.exception;

public class WebApplicationRuntimeException extends RuntimeException {

    private static final long serialVersionUID = 3845674924872653036L;

    public WebApplicationRuntimeException() {
        super();
    }

    public WebApplicationRuntimeException(String message) {
        super(message);
    }

    public WebApplicationRuntimeException(String message, Throwable cause) {
        super(message, cause);
    }

    public WebApplicationRuntimeException(Throwable cause) {
        super(cause);
    }

}

MessagesPropertiesHelper.java

package ksbysample.webapp.lending.helper.message;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;

@Component
public class MessagesPropertiesHelper {

    @Autowired
    private MessageSource messageSource;

    public String getMessage(String code, Object[] args) {
        return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
    }
    
}

BooklistCsvFileService.java

■その1

package ksbysample.webapp.lending.service.file;

import com.univocity.parsers.csv.CsvParser;
import com.univocity.parsers.csv.CsvParserSettings;
import ksbysample.webapp.lending.exception.WebApplicationRuntimeException;
import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.validation.Errors;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.regex.Pattern;

@Service
public class BooklistCsvFileService {

    private Pattern ISBN_FORMAT_PATTERN = Pattern.compile("^[0-9\\-]+$");

    @Autowired
    private MessagesPropertiesHelper messagesPropertiesHelper;

    public void validateUploadFile(MultipartFile multipartFile, Errors errors) {
        try (
                InputStream is = multipartFile.getInputStream();
                InputStreamReader isr = new InputStreamReader(is, "Windows-31J");
        ) {
            CsvParserSettings csvParserSettings = createCsvParserSettings();
            CsvParser parser = new CsvParser(csvParserSettings);
            List<String[]> allRows = parser.parseAll(isr);

            String isbn;
            int line = 1;
            for (String[] csvdata : allRows) {
                line++;

                // 項目数が 2 でない
                if (csvdata.length != 2) {
                    errors.reject("UploadBooklistForm.fileupload.lengtherr", new Object[]{line, csvdata.length}, null);
                    continue;
                }

                // ISBN のデータに数字、ハイフン以外の文字が使用されている 
                if (!ISBN_FORMAT_PATTERN.matcher(csvdata[0]).matches()) {
                    errors.reject("UploadBooklistForm.fileupload.isbn.patternerr", new Object[]{line, csvdata[0]}, null);
                }

                // ISBN のデータの文字数が 17 文字以内でない
                if (csvdata[0].length() > 17) {
                    errors.reject("UploadBooklistForm.fileupload.isbn.lengtherr", new Object[]{line, csvdata[0]}, null);
                }

                // ISBN のデータからハイフンを取り除いた文字数が 10 or 13 文字でない
                isbn = csvdata[0].replaceAll("-", "");
                if ((isbn.length() != 10) && (isbn.length() != 13)) {
                    errors.reject("UploadBooklistForm.fileupload.isbn.numlengtherr", new Object[]{line, isbn}, null);
                }

                // 書名のデータの文字数が 128 文字以内でない
                if (csvdata[1].length() > 128) {
                    errors.reject("UploadBooklistForm.fileupload.bookname.lengtherr", new Object[]{line, csvdata[1]}, null);
                }
            }
        } catch (IOException e) {
            throw new WebApplicationRuntimeException(messagesPropertiesHelper.getMessage("UploadBooklistForm.fileupload.openerr", null));
        }
    }

    private CsvParserSettings createCsvParserSettings() {
        CsvParserSettings csvParserSettings = new CsvParserSettings();
        csvParserSettings.setHeaderExtractionEnabled(true);
        csvParserSettings.getFormat().setLineSeparator("\r\n");
        return csvParserSettings;
    }
    
}

■その2

package ksbysample.webapp.lending.service.file;

import com.univocity.parsers.common.processor.BeanListProcessor;
import com.univocity.parsers.csv.CsvParser;
import com.univocity.parsers.csv.CsvParserSettings;
import ksbysample.webapp.lending.exception.WebApplicationRuntimeException;
import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.validation.Errors;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.regex.Pattern;

@Service
public class BooklistCsvFileService {

    private Pattern ISBN_FORMAT_PATTERN = Pattern.compile("^[0-9\\-]+$");

    @Autowired
    private MessagesPropertiesHelper messagesPropertiesHelper;

    public void validateUploadFile(MultipartFile multipartFile, Errors errors) {
        try (
                InputStream is = multipartFile.getInputStream();
                InputStreamReader isr = new InputStreamReader(is, "Windows-31J");
        ) {
            CsvParserSettings csvParserSettings = createCsvParserSettings();
            CsvParser parser = new CsvParser(csvParserSettings);
            List<String[]> allRows = parser.parseAll(isr);

            String isbn;
            int line = 1;
            for (String[] csvdata : allRows) {
                line++;

                // 項目数が 2 でない
                if (csvdata.length != 2) {
                    errors.reject("UploadBooklistForm.fileupload.lengtherr", new Object[]{line, csvdata.length}, null);
                    continue;
                }

                // ISBN のデータに数字、ハイフン以外の文字が使用されている 
                if (!ISBN_FORMAT_PATTERN.matcher(csvdata[0]).matches()) {
                    errors.reject("UploadBooklistForm.fileupload.isbn.patternerr", new Object[]{line, csvdata[0]}, null);
                }

                // ISBN のデータの文字数が 17 文字以内でない
                if (csvdata[0].length() > 17) {
                    errors.reject("UploadBooklistForm.fileupload.isbn.lengtherr", new Object[]{line, csvdata[0]}, null);
                }

                // ISBN のデータからハイフンを取り除いた文字数が 10 or 13 文字でない
                isbn = csvdata[0].replaceAll("-", "");
                if ((isbn.length() != 10) && (isbn.length() != 13)) {
                    errors.reject("UploadBooklistForm.fileupload.isbn.numlengtherr", new Object[]{line, isbn}, null);
                }

                // 書名のデータの文字数が 128 文字以内でない
                if (csvdata[1].length() > 128) {
                    errors.reject("UploadBooklistForm.fileupload.bookname.lengtherr", new Object[]{line, csvdata[1]}, null);
                }
            }
        } catch (IOException e) {
            throw new WebApplicationRuntimeException(messagesPropertiesHelper.getMessage("UploadBooklistForm.fileupload.openerr", null));
        }
    }

    public List<BooklistCSVRecord> convertFileToList(MultipartFile multipartFile) {
        List<BooklistCSVRecord> booklistCSVRecordList;

        try (
                InputStream is = multipartFile.getInputStream();
                InputStreamReader isr = new InputStreamReader(is, "Windows-31J");
        ) {
            CsvParserSettings csvParserSettings = createCsvParserSettings();

            // JavaBean に変換するための Processor クラスを生成して設定する
            BeanListProcessor<BooklistCSVRecord> rowProcessor = new BeanListProcessor<>(BooklistCSVRecord.class);
            csvParserSettings.setRowProcessor(rowProcessor);

            // CSVファイルを解析する
            CsvParser parser = new CsvParser(csvParserSettings);
            parser.parse(isr);
            
            // 変換した JavaBean のリストを取得する
            booklistCSVRecordList = rowProcessor.getBeans();
        } catch (IOException e) {
            throw new WebApplicationRuntimeException(messagesPropertiesHelper.getMessage("UploadBooklistForm.fileupload.openerr", null));
        }

    private CsvParserSettings createCsvParserSettings() {
        CsvParserSettings csvParserSettings = new CsvParserSettings();
        csvParserSettings.setHeaderExtractionEnabled(true);
        csvParserSettings.getFormat().setLineSeparator("\r\n");
        return csvParserSettings;
    }
    
}

UploadBooklistFormValidator.java

package ksbysample.webapp.lending.web.booklist;

import ksbysample.webapp.lending.service.file.BooklistCsvFileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

@Component
public class UploadBooklistFormValidator implements Validator {

    @Autowired
    private BooklistCsvFileService booklistCsvFileService;
    
    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.equals(UploadBooklistForm.class);
    }

    @Override
    public void validate(Object target, Errors errors) {
        UploadBooklistForm uploadBooklistForm = (UploadBooklistForm) target;
        booklistCsvFileService.validateUploadFile(uploadBooklistForm.getFileupload(), errors);
    }

}

messages_ja_JP.properties

AbstractUserDetailsAuthenticationProvider.locked=入力された ID はロックされています
AbstractUserDetailsAuthenticationProvider.disabled=入力された ID は使用できません
AbstractUserDetailsAuthenticationProvider.expired=入力された ID の有効期限が切れています
AbstractUserDetailsAuthenticationProvider.credentialsExpired=入力された ID のパスワードの有効期限が切れています
AbstractUserDetailsAuthenticationProvider.badCredentials=入力された ID あるいはパスワードが正しくありません
UserInfoUserDetailsService.usernameNotFound=入力された ID あるいはパスワードが正しくありません

typeMismatch.java.math.BigDecimal=数値を入力して下さい。
typeMismatch.java.lang.Long=数値を入力して下さい。

UploadBooklistForm.fileupload.openerr=アップロードされたCSVファイルをオープンできませんでした。
UploadBooklistForm.fileupload.lengtherr={0}行目のレコードの項目数が 2個ではありません ( {1}個 )。
UploadBooklistForm.fileupload.isbn.patternerr={0}行目のISBNのデータに数字、ハイフン以外の文字が使用されています ( {1} )。
UploadBooklistForm.fileupload.isbn.lengtherr={0}行目のISBNのデータの文字数が17文字以内でありません ( {1} )。
UploadBooklistForm.fileupload.isbn.numlengtherr={0}行目のISBNのデータの数字のみの文字数が10、13のいずれでもありません ( {1} )。
UploadBooklistForm.fileupload.bookname.lengtherr={0}行目の書名のデータの文字数が128文字以内でありません ( {1} )。
  • 以下の6つのメッセージを追加します。
    • UploadBooklistForm.fileupload.openerr
    • UploadBooklistForm.fileupload.lengtherr
    • UploadBooklistForm.fileupload.isbn.patternerr
    • UploadBooklistForm.fileupload.isbn.lengtherr
    • UploadBooklistForm.fileupload.isbn.numlengtherr
    • UploadBooklistForm.fileupload.bookname.lengtherr

booklist.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>貸出希望書籍 CSV ファイルアップロード</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"/>
    <!-- Bootstrap File Input -->
    <link href="/css/fileinput.min.css" rel="stylesheet" type="text/css"/>

    <style type="text/css">
        <!--
        .callout ul li {
            margin-left: -30px;
        }
        -->
    </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>貸出希望書籍 CSV ファイルアップロード</h1>
            </section>

            <!-- Main content -->
            <section class="content">
                <div class="row">
                    <div class="col-xs-12">
                        <form id="uploadBooklistForm" enctype="multipart/form-data" method="post" action="/booklist/fileupload" th:action="@{/booklist/fileupload}" th:object="${uploadBooklistForm}">
                            <div class="form-group">
                                <input type="file" name="fileupload" class="js-fileupload"/>
                            </div>
                            <div class="callout callout-danger" th:if="${#fields.hasGlobalErrors()}">
                                <h4><i class="fa fa-warning"></i> アップロードされたCSVファイルでエラーが発生しました。</h4>
                                <ul th:each="err : ${#fields.globalErrors()}">
                                    <li th:text="${err}">エラーメッセージ</li>
                                </ul>
                            </div>
                        </form>
                    </div>
                </div>
            </section>
            <!-- /.content -->
        </div>
        <!-- /.container -->
    </div>

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

<script th:replace="common/bottom-js"></script>
<!-- Bootstrap File Input -->
<script src="/js/fileinput.min.js" type="text/javascript"></script>
<script src="/js/fileinput_locale_ja.js" type="text/javascript"></script>
<script type="text/javascript">
    <!--
    $(document).ready(function() {
        $('.js-fileupload').fileinput({
            language: 'ja',
            showPreview: false,
            maxFileCount: 1,
            browseClass: 'btn btn-info fileinput-browse-button',
            browseIcon: '',
            browseLabel: ' ファイル選択',
            removeClass: 'btn btn-warning',
            removeIcon: '',
            removeLabel: ' 削除',
            uploadClass: 'btn btn-success fileinput-upload-button',
            uploadIcon: '<i class="fa fa-upload"></i>',
            uploadLabel: ' アップロード',
            allowedFileExtensions: ['csv'],
            msgValidationError: '<span class="text-danger"><i class="fa fa-warning"></i> CSV ファイルのみ有効です。'
        })
    });
    -->
</script>
</body>
</html>
  • <form>タグに th:object="${uploadBooklistForm}" を追加します。
  • <div class="callout callout-danger">...</div><form>...</form> の中に移動します。
  • `<div class="callout callout-danger">th:if="${#fields.hasGlobalErrors()}" を追加します。
  • <div class="callout callout-danger">...</div> の中の ul, li タグを上記のソースのように変更します。

テスト2.csv

"ISBN","書名"
"978-4-7741-6366-6","GitHub実践入門","項目が3つ"
"978-4-7741-5x77-3","JUnit実践入門"
"978-4-79173-8014-9","Java最強リファレンス"
"978-4-719734778-4","アジャイルソフトウェア開発の奥義"
"978-4-87311-704-1","123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"

BooklistCSVRecord.java

package ksbysample.webapp.lending.service.file;

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

@Data
public class BooklistCSVRecord {

    @Parsed
    private String isbn;

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

}
  • CSVファイルのデータを関連付けるフィールドに uniVocity-parses の @Parsed アノテーションを付加します。カラム名とフィールド名が異なる場合には @Parsed(field = "書名") のように field 属性に実際のカラム名を記述します。

LendingAppStatusValues.java

package ksbysample.webapp.lending.values;

import lombok.Getter;
import org.apache.commons.lang3.StringUtils;

@Getter
public enum LendingAppStatusValues {

    TENPORARY_SAVE("1", "一時保存")
    , PENDING("2", "申請中")
    , APPLOVED("3", "承認済");

    private final String value;
    private final String text;

    LendingAppStatusValues(String value, String text) {
        this.value = value;
        this.text = text;
    }

    public static String getText(String value) {
        String result = "";
        for (LendingAppStatusValues val : LendingAppStatusValues.values()) {
            if (StringUtils.equals(val.getValue(), value)) {
                result = val.getText();
            }
        }

        return result;
    }

}

LendingUser.java

package ksbysample.webapp.lending.security;

import ksbysample.webapp.lending.entity.UserInfo;
import lombok.Data;
import org.springframework.beans.BeanUtils;

import java.io.Serializable;
import java.time.LocalDateTime;

@Data
public class LendingUser implements Serializable {

    private static final long serialVersionUID = 511849715573196163L;

    Long userId;

    String username;

    String password;

    String mailAddress;

    Short enabled;

    Short cntBadcredentials;

    LocalDateTime expiredAccount;

    LocalDateTime expiredPassword;

    public LendingUser(UserInfo userInfo) {
        BeanUtils.copyProperties(userInfo, this);
    }
    
}
  • Long userId; を追加します。

LendingUserDetails.java

package ksbysample.webapp.lending.security;

import ksbysample.webapp.lending.entity.UserInfo;
import org.springframework.beans.BeanUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Set;

public class LendingUserDetails implements UserDetails {

    private static final long serialVersionUID = 4775912062739295150L;

    private LendingUser lendingUser;
    private final Set<? extends GrantedAuthority> authorities;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final boolean enabled;

    public LendingUserDetails(UserInfo userInfo
            , Set<? extends GrantedAuthority> authorities) {
        LocalDateTime now = LocalDateTime.now();
        lendingUser = new LendingUser(userInfo);
        this.authorities = authorities;
        this.accountNonExpired = !userInfo.getExpiredAccount().isBefore(now);
        this.accountNonLocked = (userInfo.getCntBadcredentials() < 5);
        this.credentialsNonExpired = !userInfo.getExpiredPassword().isBefore(now);
        this.enabled = (userInfo.getEnabled() == 1);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return lendingUser.getPassword();
    }

    public Long getUserId() {
        return lendingUser.getUserId();
    }

    @Override
    public String getUsername() {
        return lendingUser.getMailAddress();
    }

    public String getName() {
        return lendingUser.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

create_table.sql

create table lending_app (
    lending_app_id          bigserial primary key
    , status                varchar(1) not null
    , lending_user_id       bigint not null references user_info(user_id)
    , approval_user_id      bigint references user_info(user_id)
);
  • approval_user_id から not null 制約を削除します。

RegisterBooklistForm.java

package ksbysample.webapp.lending.web.booklist;

import lombok.Data;

import java.util.List;

@Data
public class RegisterBooklistForm {

    private List<RegisterBooklistRow> registerBooklistRowList;
    
    private Long lendingAppId;
    
    @Data
    public class RegisterBooklistRow {
        private int no;
        private String isbn;
        private String bookName;        
    }

}

LendingBookDao.java

package ksbysample.webapp.lending.dao;

import ksbysample.webapp.lending.entity.LendingBook;
import ksbysample.webapp.lending.util.doma.ComponentAndAutowiredDomaConfig;
import org.seasar.doma.Dao;
import org.seasar.doma.Delete;
import org.seasar.doma.Insert;
import org.seasar.doma.Select;
import org.seasar.doma.Update;

import java.util.List;

/**
 */
@Dao
@ComponentAndAutowiredDomaConfig
public interface LendingBookDao {

    /**
     * @param lendingBookId
     * @return the LendingBook entity
     */
    @Select
    LendingBook selectById(Long lendingBookId);

    @Select
    List<LendingBook> selectByLendingAppId(Long lendingAppId);
    
    /**
     * @param entity
     * @return affected rows
     */
    @Insert
    int insert(LendingBook entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Update
    int update(LendingBook entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Delete
    int delete(LendingBook entity);
}
  • List<LendingBook> selectByLendingAppId(Long lendingAppId); を追加します。

selectByLendingAppId.sql

select
  /*%expand*/*
from
  lending_book
where
  lending_app_id = /* lendingAppId */1
order by
  lending_book_id

BooklistService.java

■その1

package ksbysample.webapp.lending.web.booklist;

import ksbysample.webapp.lending.dao.LendingAppDao;
import ksbysample.webapp.lending.dao.LendingBookDao;
import ksbysample.webapp.lending.entity.LendingApp;
import ksbysample.webapp.lending.entity.LendingBook;
import ksbysample.webapp.lending.security.LendingUserDetails;
import ksbysample.webapp.lending.service.file.BooklistCSVRecord;
import ksbysample.webapp.lending.service.file.BooklistCsvFileService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.List;

import static ksbysample.webapp.lending.values.LendingAppStatusValues.TENPORARY_SAVE;

@Service
public class BooklistService {

    @Autowired
    private BooklistCsvFileService booklistCsvFileService;

    @Autowired
    private LendingAppDao lendingAppDao;

    @Autowired
    private LendingBookDao lendingBookDao;
    
    public Long temporarySaveBookListCsvFile(UploadBooklistForm uploadBooklistForm) {
        // アップロードされたCSVファイルのデータを List に変換する
        List<BooklistCSVRecord> booklistCSVRecordList
                = booklistCsvFileService.convertFileToList(uploadBooklistForm.getFileupload());

        // 現在ログインしているユーザ情報を取得する
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        LendingUserDetails lendingUserDetails = (LendingUserDetails) auth.getPrincipal();
        
        // lending_app テーブルにデータを保存する
        LendingApp lendingApp = new LendingApp();
        lendingApp.setStatus(TENPORARY_SAVE.getValue());
        lendingApp.setLendingUserId(lendingUserDetails.getUserId());
        lendingAppDao.insert(lendingApp);

        // lending_book テーブルにデータを保存する
        LendingBook lendingBook;
        for (BooklistCSVRecord booklistCSVRecord : booklistCSVRecordList) {
            lendingBook = new LendingBook();
            BeanUtils.copyProperties(booklistCSVRecord, lendingBook);
            lendingBook.setLendingAppId(lendingApp.getLendingAppId());
            lendingBookDao.insert(lendingBook);
        }

        return lendingApp.getLendingAppId();
    }
    
}

■その2

package ksbysample.webapp.lending.web.booklist;

import ksbysample.webapp.lending.dao.LendingAppDao;
import ksbysample.webapp.lending.dao.LendingBookDao;
import ksbysample.webapp.lending.entity.LendingApp;
import ksbysample.webapp.lending.entity.LendingBook;
import ksbysample.webapp.lending.security.LendingUserDetails;
import ksbysample.webapp.lending.service.file.BooklistCSVRecord;
import ksbysample.webapp.lending.service.file.BooklistCsvFileService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.List;

import static ksbysample.webapp.lending.values.LendingAppStatusValues.TENPORARY_SAVE;

@Service
public class BooklistService {

    @Autowired
    private BooklistCsvFileService booklistCsvFileService;

    @Autowired
    private LendingAppDao lendingAppDao;

    @Autowired
    private LendingBookDao lendingBookDao;
    
    public Long temporarySaveBookListCsvFile(UploadBooklistForm uploadBooklistForm) {
        // アップロードされたCSVファイルのデータを List に変換する
        List<BooklistCSVRecord> booklistCSVRecordList
                = booklistCsvFileService.convertFileToList(uploadBooklistForm.getFileupload());

        // 現在ログインしているユーザ情報を取得する
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        LendingUserDetails lendingUserDetails = (LendingUserDetails) auth.getPrincipal();
        
        // lending_app テーブルにデータを保存する
        LendingApp lendingApp = new LendingApp();
        lendingApp.setStatus(TENPORARY_SAVE.getValue());
        lendingApp.setLendingUserId(lendingUserDetails.getUserId());
        lendingAppDao.insert(lendingApp);

        // lending_book テーブルにデータを保存する
        LendingBook lendingBook;
        for (BooklistCSVRecord booklistCSVRecord : booklistCSVRecordList) {
            lendingBook = new LendingBook();
            BeanUtils.copyProperties(booklistCSVRecord, lendingBook);
            lendingBook.setLendingAppId(lendingApp.getLendingAppId());
            lendingBookDao.insert(lendingBook);
        }

        return lendingApp.getLendingAppId();
    }

    public List<LendingBook> getLendingBookList(Long lendingAppId) {
        List<LendingBook> lendingBookList = lendingBookDao.selectByLendingAppId(lendingAppId);
        return lendingBookList;
    }
    
}

fileupload.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>貸出希望書籍 CSV ファイルアップロード</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 type="text/css">
        <!--
        .box-body.no-padding {
            padding-bottom: 10px !important;
        }
        -->
    </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>貸出希望書籍 CSV ファイルアップロード</h1>
            </section>

            <!-- Main content -->
            <section class="content">
                <div class="row">
                    <div class="col-xs-12">
                        <div class="box">
                            <div class="box-body no-padding">
                                <form id="registerBooklistForm" method="post" action="/booklist/register" th:action="@{/booklist/register}" th:object="${registerBooklistForm}">
                                    <table class="table table-hover">
                                        <colgroup>
                                            <col width="5%"/>
                                            <col width="35%"/>
                                            <col width="60%"/>
                                        </colgroup>
                                        <thead class="bg-purple">
                                        <tr>
                                            <th>No.</th>
                                            <th>ISBN</th>
                                            <th>書名</th>
                                        </tr>
                                        </thead>
                                        <tbody class="jp-gothic">
                                        <tr th:each="row, iterStat : *{registerBooklistRowList}">
                                            <th th:text="${iterStat.count}">1</th>
                                            <th th:text="${row.isbn}">978-1-4302-5908-4</th>
                                            <th th:text="${row.bookName}">Spring Recipes</th>
                                        </tr>
                                        </tbody>
                                    </table>
                                    <input type="hidden" th:value="*{lendingAppId}"/>
                                    <div class="text-center">
                                        <button class="btn bg-blue js-btn-register"><i class="fa fa-save"></i> 登録</button>
                                        <button class="btn bg-orange js-btn-backindex"><i class="fa fa-undo"></i> ファイルをアップロードし直す</button>
                                    </div>
                                </form>
                            </div>
                        </div>
                    </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-register").click(function(){
            $("#registerBooklistForm").submit();
           return false;
        });

        $(".js-btn-backindex").click(function(){
            location.href = "/booklist";
            return false;
        });
    });
    -->
</script>
</body>
</html>
  • <form id="registerBooklistForm" ...>th:object="${registerBooklistForm}" を追加します。
  • tr タグに th:each="row, iterStat : *{registerBooklistRowList}" を追加します。
  • th タグに th:text="${...}" を追加し、データが表示されるようにします。No. のカラムは Thymeleaf の機能を利用して表示します。
  • <input type="hidden" th:value="*{lendingAppId}"/> を追加します。

履歴

2015/09/27
初版発行。