Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その23 )( 貸出希望書籍 CSV ファイルアップロード画面の作成2 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その22 )( 貸出希望書籍 CSV ファイルアップロード画面の作成 ) の続きです。
参照したサイト・書籍
Uploading Files
https://spring.io/guides/gs/uploading-files/- Spring の公式サイトのファイルアップロードの実装方法に関する記事です。
uniVocity/univocity-parsers
https://github.com/uniVocity/univocity-parsersABOUT UNIVOCITY-PARSERS
http://www.univocity.com/pages/about-parsers- uniVocity-parses の公式サイトのトップページです。
UNIVOCITY-PARSERS DOCUMENTATION
http://www.univocity.com/pages/parsers-documentation- uniVocity-parses の公式サイトの Documentation ページです。ここに Tutorials や Javadocs へのリンクがあります。
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 から順にインクリメントした数字を表示するために使用しています。
目次
手順
CSVファイルアップロード機能の作成
テスト用CSVファイルを用意する
アップロードする CSV ファイルは以下の仕様とします。
- 文字コードは Windows-31J。
- 改行コードは CR+LF。
- 項目は必ずダブルクォーテーションで囲む。
- 項目はISBN、書名の2項目固定。
- 1行目はヘッダ行とし、"ISBN","書名" と記述する。
UploadBooklistForm クラスを作成し、ファイルをアップロードできるようにする
アップロードファイルの最大サイズを設定しようと思い、src/main/resources の下の application.properties を開いて "multipart." を入力して候補を表示させてみたところ、以下の画像の内容が表示されました。
multipart.max-file-size の設定は 1MB になっており、この値で十分なので特に設定は行わないことにします。
※multipart の設定項目については Appendix A. Common application properties に記載されています。IntelliJ IDEA の候補では multipart.enabled の設定値が false で表示されていますが、Appendix A を見るとデフォルト値は true でしたのでこちらも特に設定はしません。
src/main/java/ksbysample/webapp/lending/web/booklist の下に UploadBooklistForm.java を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/web/booklist の下の BooklistController.java を リンク先のその1の内容 に変更します。
これでファイルをアップロードするとコンソールにファイルの内容が出力されるようになります。動作確認してみます。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザから http://localhost:8080/booklist/ にアクセスして テスト.csv をアップロードすると、コンソールにファイルの内容が出力されることが確認できます。
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
です。
以下の手順で確認しました。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
環境変数 TEMP に設定されているディレクトリ ( 例 C:\Users\root\AppData\Local\Temp ) の下のディレクトリ・ファイルの内、削除可能なものを全て削除します。
ブラウザから http://localhost:8080/booklist/ にアクセスしてファイルをアップロードするとエラーになります。
ログを見ると
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 を利用します。
build.gradle を リンク先の内容 に変更します。
Gradle projects View の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
UploadBooklistFormValidator クラスを作成し、アップロードされたファイルをチェックする
Web アプリケーションで汎用的に使用する RuntimeException の継承クラスを作成します。src/main/java/ksbysample/webapp/lending の下に exception パッケージを作成します。
src/main/java/ksbysample/webapp/lending/exception の下に WebApplicationRuntimeException.java を作成します。作成後、リンク先の内容 に変更します。
messages_ja_JP.properties からメッセージを取得する Helper クラスを作成します。src/main/java/ksbysample/webapp/lending/helper の下に message パッケージを作成します。
src/main/java/ksbysample/webapp/lending/helper/message の下に MessagesPropertiesHelper.java を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/service の下に file パッケージを作成します。
src/main/java/ksbysample/webapp/lending/service/file の下に BooklistCsvFileService.java を作成します。作成後、リンク先のその1の内容 に変更します。
src/main/java/ksbysample/webapp/lending/web/booklist の下に UploadBooklistFormValidator.java を作成します。作成後、リンク先の内容 に変更します。
src/main/resources の下の messages_ja_JP.properties を リンク先の内容 に変更します。
src/main/resources/templates/booklist の下の booklist.html を リンク先の内容 に変更します。
動作確認してみます。Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
エラーになる CSV ファイルを用意します。デスクトップにテスト.csv をコピーして「テスト2.csv」というファイルを作成します。作成後、リンク先の内容 に変更します。
ブラウザから http://localhost:8080/booklist/ にアクセスして テスト2.csv をアップロードすると、以下のエラーメッセージが表示されることが確認できます。
Ctrl+F2 を押して Tomcat を停止します。
アップロードされたCSVファイルのデータを DB に保存する
src/main/java/ksbysample/webapp/lending/service/file の下に BooklistCSVRecord.java を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/service/file の下の BooklistCsvFileService.java を リンク先のその2の内容 に変更します。
lending_app テーブルの status にセットする値を定義する Enum を作成します。src/main/java/ksbysample/webapp/lending の下に values パッケージを作成します。
src/main/java/ksbysample/webapp/lending/values の下に LendingAppStatusValues.java を作成します。作成後、リンク先の内容 に変更します。
lending_app テーブルの lending_user_id に user_info テーブルの user_id を保存するのですが、ログインしているユーザの user_info.user_id を取得するための実装が漏れていたので追加します。src/main/java/ksbysample/webapp/lending/security の下の LendingUser.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/security の下の LendingUserDetails.java を リンク先の内容 に変更します。
lending_app テーブルの approval_user_id に not null 制約が付いているのですが承認するまでは空なので not null 制約を外します。/sql の下の create_table.sql を リンク先の内容 に変更します。
以下のコマンドを実行し 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 テーブルも作成し直しています。
src/main/java/ksbysample/webapp/lending/web/booklist の下に RegisterBooklistForm.java を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/dao の下の LendingBookDao.java を リンク先の内容 に変更します。
src/main/resources/META-INF/ksbysample/webapp/lending/dao/LendingBookDao の下に selectByLendingAppId.sql を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/web/booklist の下に BooklistService.java を作成します。作成後、リンク先のその1の内容 に変更します。
src/main/java/ksbysample/webapp/lending/web/booklist の下の BooklistController.java を リンク先のその2の内容 に変更します。
次の画面に遷移しアップロードされたCSVファイルのデータを表示する
src/main/java/ksbysample/webapp/lending/web/booklist の下に BooklistService.java を作成します。作成後、リンク先のその2の内容 に変更します。
src/main/java/ksbysample/webapp/lending/web/booklist の下の BooklistController.java を リンク先のその3の内容 に変更します。
src/main/resources/templates/booklist の下の fileupload.html を リンク先の内容 に変更します。
動作確認
動作確認します。Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動し http://localhost:8080/booklist へアクセスします。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。
テスト.csv をアップロードします。
次の画面にアップロードしたファイルの内容が表示されます。
lending_app、lending_book テーブルにもデータが登録されていることが確認できます。
Ctrl+F2 を押して Tomcat を停止します。
一旦 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; } }
- convertFileToList メソッドを追加します。
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; } }
- getUserId メソッドを追加します。
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; } }
- getLendingBookList メソッドを追加します。
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
初版発行。