かんがるーさんの日記

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

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その28 )( 貸出状況取得タスクの作成2 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その27 )( 貸出状況取得タスクの作成 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 貸出希望書籍 CSV ファイルアップロード画面でアップロードした CSV ファイルの ISBN 重複チェック処理の作成
    • 貸出状況取得タスクの作成
      • メール送信処理の作成

参照したサイト・書籍

  1. google/guava - NewCollectionTypesExplained
    https://github.com/google/guava/wiki/NewCollectionTypesExplained

目次

  1. 貸出希望書籍 CSV ファイルアップロード画面でアップロードした CSV ファイルの ISBN 重複チェック処理の作成
  2. Velocity のテンプレートファイルの作成
  3. メール生成用ヘルパークラスの作成
  4. ksbysample-webapp-email から EmailService.java を持ってくる
  5. メール送信処理の作成
  6. 動作確認
  7. 次回は。。。

手順

貸出希望書籍 CSV ファイルアップロード画面でアップロードした CSV ファイルの ISBN 重複チェック処理の作成

  1. 重複チェックエラー時のメッセージを追加します。src/main/resources の下の messages_ja_JP.properties を リンク先の内容 に変更します。

  2. 重複チェック処理を追加します。src/main/java/ksbysample/webapp/lending/service/file の下の BooklistCsvFileService.javaリンク先の内容 に変更します。

  3. 動作確認します。最初にテストデータとして リンク先の内容 のテスト3.csv を用意します。

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

  5. http://localhost:8080/booklist からテスト3.csv をアップロードします。

    f:id:ksby:20151030000950p:plain

    978-4-7973-8014-9 が2件、978-4-7741-6366-6 が3件重複しているメッセージが表示されることが確認できます。

    f:id:ksby:20151030001116p:plain

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

Velocity のテンプレートファイルの作成

  1. src/main/resources/templates の下に mail ディレクトリを作成します。

  2. src/main/resources/templates/mail の下に mail001-body.vm を作成します。作成後、リンク先の内容 に変更します。

メール生成用ヘルパークラスの作成

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

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

ksbysample-webapp-email から EmailService.java を持ってくる

  1. 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 の下に配置します。リンク先の内容 です。

メール送信処理の作成

  1. src/main/java/ksbysample/webapp/lending/listener/rabbitmq の下の InquiringStatusOfBookQueueListener.javaリンク先の内容 に変更します。

動作確認

  1. 動作確認します。メールサーバとして smtp4dev を起動します。smtp4dev のインストール方法は Spring Boot でメール送信する Web アプリケーションを作る ( その2 )( PostgreSQL 9.4.1、smtp4dev のインストール ) を参照してください。

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

  3. http://localhost:8080/booklist からテスト.csv をアップロードします。

    f:id:ksby:20151030064508p:plain

    確認画面が表示されたら「登録」ボタンをクリックします。

    f:id:ksby:20151030064622p:plain

    登録されて貸出申請ID が表示されます。

    f:id:ksby:20151030064748p:plain

  4. リスナーで処理が実行されて smtp4dev にメールが届きます。

    f:id:ksby:20151030064934p:plain

    届いたメールをダブルクリックして表示すると、Velocity のテンプレートファイル通りの内容で表示されていることが確認できます。

    f:id:ksby:20151030065043p:plain

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

  6. smtp4dev に届いたメールを削除した後、smtp4dev を終了します。

  7. 一旦 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 を呼び出してエラーメッセージをセットします。

テスト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
初版発行。