Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その28 )( 貸出状況取得タスクの作成2 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その27 )( 貸出状況取得タスクの作成 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
参照したサイト・書籍
google/guava - NewCollectionTypesExplained
https://github.com/google/guava/wiki/NewCollectionTypesExplained- ISBN の重複チェックで Multiset を使用するために参照しました。
目次
- 貸出希望書籍 CSV ファイルアップロード画面でアップロードした CSV ファイルの ISBN 重複チェック処理の作成
- Velocity のテンプレートファイルの作成
- メール生成用ヘルパークラスの作成
- ksbysample-webapp-email から EmailService.java を持ってくる
- メール送信処理の作成
- 動作確認
- 次回は。。。
手順
貸出希望書籍 CSV ファイルアップロード画面でアップロードした CSV ファイルの ISBN 重複チェック処理の作成
重複チェックエラー時のメッセージを追加します。src/main/resources の下の messages_ja_JP.properties を リンク先の内容 に変更します。
重複チェック処理を追加します。src/main/java/ksbysample/webapp/lending/service/file の下の BooklistCsvFileService.java を リンク先の内容 に変更します。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
http://localhost:8080/booklist からテスト3.csv をアップロードします。
978-4-7973-8014-9 が2件、978-4-7741-6366-6 が3件重複しているメッセージが表示されることが確認できます。
Ctrl+F2 を押して Tomcat を停止します。
Velocity のテンプレートファイルの作成
src/main/resources/templates の下に mail ディレクトリを作成します。
src/main/resources/templates/mail の下に mail001-body.vm を作成します。作成後、リンク先の内容 に変更します。
メール生成用ヘルパークラスの作成
src/main/java/ksbysample/webapp/lending/helper の下に mail パッケージを作成します。
src/main/java/ksbysample/webapp/lending/helper/mail の下に Mail001Helper.java を作成します。作成後、リンク先の内容 に変更します。
ksbysample-webapp-email から EmailService.java を持ってくる
- https://github.com/ksby/ksbysample-webapp-email/tree/1.0.x/src/main/java/ksbysample/webapp/email/service の下にある EmailService.java を持ってきて、src/main/java/ksbysample/webapp/lending/service の下に配置します。リンク先の内容 です。
メール送信処理の作成
- src/main/java/ksbysample/webapp/lending/listener/rabbitmq の下の InquiringStatusOfBookQueueListener.java を リンク先の内容 に変更します。
動作確認
動作確認します。メールサーバとして smtp4dev を起動します。smtp4dev のインストール方法は Spring Boot でメール送信する Web アプリケーションを作る ( その2 )( PostgreSQL 9.4.1、smtp4dev のインストール ) を参照してください。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
http://localhost:8080/booklist からテスト.csv をアップロードします。
確認画面が表示されたら「登録」ボタンをクリックします。
登録されて貸出申請ID が表示されます。
リスナーで処理が実行されて smtp4dev にメールが届きます。
届いたメールをダブルクリックして表示すると、Velocity のテンプレートファイル通りの内容で表示されていることが確認できます。
Ctrl+F2 を押して Tomcat を停止します。
smtp4dev に届いたメールを削除した後、smtp4dev を終了します。
一旦 commit します。
次回は。。。
- 貸出状況取得タスクで作成したクラスのテストを書きます。
ソースコード
messages_ja_JP.properties
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.isbn.duplicateerr={0} のISBNのデータが {1}件重複しています。 UploadBooklistForm.fileupload.bookname.lengtherr={0}行目の書名のデータの文字数が128文字以内でありません ( {1} )。
- UploadBooklistForm.fileupload.isbn.duplicateerr を追加します。。
BooklistCsvFileService.java
package ksbysample.webapp.lending.service.file; import com.google.common.collect.HashMultiset; import com.google.common.collect.Maps; import com.google.common.collect.Multiset; 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.Map; import java.util.regex.Pattern; import java.util.stream.Collectors; @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; Multiset<String> isbnList = HashMultiset.create(); 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); } // ISBN を重複チェック用リストに追加する isbnList.add(csvdata[0]); } // 重複している ISBN があればエラーメッセージをセットする isbnList.stream() .filter(str -> isbnList.count(str) >= 2) .distinct() .forEach(str -> errors.reject("UploadBooklistForm.fileupload.isbn.duplicateerr" , new Object[]{str, isbnList.count(str)}, null)); } catch (IOException e) { throw new WebApplicationRuntimeException(messagesPropertiesHelper.getMessage("UploadBooklistForm.fileupload.openerr", null)); } }
- validateUploadFile メソッドの以下の点を変更します。
- for ループの前に
Multiset<String> isbnList = HashMultiset.create();
を追加します。 - for ループ内に
isbnList.add(csvdata[0]);
を追加します。 - for ループの後に
isbnList.stream()...
の処理を追加し、ISBN が2回以上出現している場合には errors.reject を呼び出してエラーメッセージをセットします。
- for ループの前に
テスト3.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-7973-8014-9","Java最強リファレンス" "978-4-87311-704-1","Javaによる関数型プログラミング" "978-4-7741-6366-6","GitHub実践入門" "978-4-7741-6366-6","GitHub実践入門"
- 978-4-7973-8014-9 が2件、978-4-7741-6366-6 が3件重複しています。
mail001-body.vm
貸出状況を確認しました。以下のURLから借りたい書籍を申請してください。 http://localhost:8080/lendingapp?lendingAppId=${lendingAppId}
Mail001Helper.java
package ksbysample.webapp.lending.helper.mail; import ksbysample.webapp.lending.util.velocity.VelocityUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Component; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.util.HashMap; import java.util.Map; @Component public class Mail001Helper { private final String TEMPLATE_LOCATION_TEXTMAIL = "mail/mail001-body.vm"; private final String FROM_ADDR = "StatusOfBookChecker@sample.com"; private final String SUBJECT = "貸出状況を確認しました"; @Autowired private VelocityUtils velocityUtils; @Autowired private JavaMailSender mailSender; public MimeMessage createMessage(String toAddr, Long lendingAppId) throws MessagingException { MimeMessage mimeMessage = this.mailSender.createMimeMessage(); MimeMessageHelper message = new MimeMessageHelper(mimeMessage, false, "UTF-8"); message.setFrom(FROM_ADDR); message.setTo(toAddr); message.setSubject(SUBJECT); message.setText(generateTextUsingVelocity(lendingAppId), false); return message.getMimeMessage(); } private String generateTextUsingVelocity(Long lendingAppId) { Map<String, Object> model = new HashMap<>(); model.put("lendingAppId", lendingAppId); return velocityUtils.merge(this.TEMPLATE_LOCATION_TEXTMAIL, model); } }
EmailService.java
package ksbysample.webapp.email.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; import javax.mail.internet.MimeMessage; @Service public class EmailService { @Autowired private JavaMailSender mailSender; public void sendSimpleMail(SimpleMailMessage mailMessage) { mailSender.send(mailMessage); } public void sendMail(MimeMessage message) { mailSender.send(message); } }
InquiringStatusOfBookQueueListener.java
package ksbysample.webapp.lending.listener.rabbitmq; import ksbysample.webapp.lending.config.Constant; import ksbysample.webapp.lending.dao.LendingAppDao; import ksbysample.webapp.lending.dao.LendingBookDao; import ksbysample.webapp.lending.dao.LibraryForsearchDao; import ksbysample.webapp.lending.dao.UserInfoDao; import ksbysample.webapp.lending.entity.LendingApp; import ksbysample.webapp.lending.entity.LendingBook; import ksbysample.webapp.lending.entity.LibraryForsearch; import ksbysample.webapp.lending.entity.UserInfo; import ksbysample.webapp.lending.helper.mail.Mail001Helper; import ksbysample.webapp.lending.service.EmailService; import ksbysample.webapp.lending.service.calilapi.Book; import ksbysample.webapp.lending.service.calilapi.CalilApiService; import ksbysample.webapp.lending.service.queue.InquiringStatusOfBookQueueMessage; import ksbysample.webapp.lending.service.queue.InquiringStatusOfBookQueueService; import ksbysample.webapp.lending.values.LendingAppStatusValues; import org.apache.commons.lang3.StringUtils; import org.seasar.doma.jdbc.SelectOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.util.List; import java.util.stream.Collectors; @Component public class InquiringStatusOfBookQueueListener { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private InquiringStatusOfBookQueueService inquiringStatusOfBookQueueService; @Autowired private CalilApiService calilApiService; @Autowired private EmailService emailService; @Autowired private Mail001Helper mail001Helper; @Autowired private LibraryForsearchDao libraryForsearchDao; @Autowired private LendingAppDao lendingAppDao; @Autowired private LendingBookDao lendingBookDao; @Autowired private UserInfoDao userInfoDao; @RabbitListener(queues = {Constant.QUEUE_NAME_INQUIRING_STATUSOFBOOK}) public void receiveMessage(Message message) throws MessagingException { // 受信したメッセージを InquiringStatusOfBookQueueMessage クラスのインスタンスに変換する InquiringStatusOfBookQueueMessage convertedMessage = inquiringStatusOfBookQueueService.convertMessageToObject(message); // 選択中の図書館を取得する LibraryForsearch libraryForsearch = libraryForsearchDao.selectSelectedLibrary(); // 更新対象の lending_app テーブルのデータを取得する LendingApp lendingApp = lendingAppDao.selectById(convertedMessage.getLendingAppId(), SelectOptions.get().forUpdate()); if (lendingApp == null) { logger.error("lending_app テーブルに対象のデータがありませんでした ( lending_app_id = {} )。", convertedMessage.getLendingAppId()); return; } // lending_book テーブルから調査対象の ISBN 一覧を取得する List<LendingBook> lendingBookList = lendingBookDao.selectByLendingAppId(convertedMessage.getLendingAppId(), SelectOptions.get().forUpdate()); if (lendingBookList == null) { logger.error("lending_book テーブルに対象のデータがありませんでした ( lending_app_id = {} )。", convertedMessage.getLendingAppId()); return; } List<String> isbnList = lendingBookList.stream() .map(LendingBook::getIsbn) .collect(Collectors.toList()); // カーリルの蔵書検索 WebAPI を呼び出して貸出状況を取得する List<Book> bookList = calilApiService.check(libraryForsearch.getSystemid(), isbnList); // lending_book テーブルに取得した貸出状況を反映し、lending_app テーブルの status を 2(未申請) に更新する copyLendingStateFromBookListToEntityList(bookList, lendingBookList); updateLendingData(lendingBookList, lendingApp); // データを登録したユーザへメールを送信する UserInfo userInfo = userInfoDao.selectById(lendingApp.getLendingUserId()); MimeMessage mimeMessage = mail001Helper.createMessage(userInfo.getMailAddress(), convertedMessage.getLendingAppId()); emailService.sendMail(mimeMessage); } private void copyLendingStateFromBookListToEntityList(List<Book> bookList, List<LendingBook> lendingBookList) { for (LendingBook lendingBook : lendingBookList) { for (Book book : bookList) { if (StringUtils.equals(lendingBook.getIsbn(), book.getIsbn())) { lendingBook.setLendingState(book.getFirstLibkeyValue()); break; } } } } private void updateLendingData(List<LendingBook> lendingBookList, LendingApp lendingApp) { // lending_book テーブルに取得した貸出状況を反映する for (LendingBook lendingBook : lendingBookList) { lendingBookDao.updateLendingState(lendingBook); } // lending_app テーブルの status を 2(未申請) に更新する lendingApp.setStatus(LendingAppStatusValues.UNAPPLIED.getValue()); lendingAppDao.update(lendingApp); } }
- @Autowired アノテーションを付加した以下のフィールドを追加します。
- private EmailService emailService;
- private Mail001Helper mail001Helper;
- private UserInfoDao userInfoDao;
- receiveMessage メソッドに
throws MessagingException
を追加します。 - メール送信処理 ( 「データを登録したユーザへメールを送信する」のコメント以降 ) を追加します。
履歴
2015/10/30
初版発行。