Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その36 )( 貸出申請画面の作成7 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その35 )( 貸出申請画面の作成6 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 貸出申請画面の作成
- 承認者にメールを送信する機能の実装
- 貸出申請画面の作成
事情があり2週間以上更新できませんでしたが、今日から復活です。
参照したサイト・書籍
TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.0.1.RELEASE documentation - 4.2. ドメイン層の実装 - 4.2.5.1. Serviceの役割
http://terasolunaorg.github.io/guideline/5.0.1.RELEASE/ja/ImplementationAtEachLayer/DomainLayer.html#id17- クラスの分け方をどうすればよいのかを知るためにいろいろなドキュメントを見ているのですが、TERASOLUNA のマニュアルの上の章に「他のServiceクラスのメソッドを呼び出すことは、原則禁止とする」というのがありました。
- 現在 EmailService クラスは Service クラスとして実装して他の Service クラスから呼び出すようにしていますが、この前書いた Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その33 )( 貸出申請画面の作成4 ) の helper クラスと utility クラスの分け方を考えると helper クラスに変更するのが良さそうなので、今回変更します。
Is there any generic version of toArray() in Guava or Apache Commons Collections?
http://stackoverflow.com/questions/21729668/is-there-any-generic-version-of-toarray-in-guava-or-apache-commons-collections- List → String[] に変換する方法を調査した時に参照しました。
- Guava の Iterables.toArray() を使用します。
目次
- EmailService → EmailHelper クラスへ変更する
- Velocity のテンプレートファイルの作成
- メール生成用ヘルパークラスの作成
- ユーザ情報用ヘルパークラスの作成
- LendingappService クラスの変更
- LendingappController クラスの変更
- 動作確認
手順
EmailService → EmailHelper クラスへ変更する
IntelliJ IDEA のリファクタリングの機能を利用して EmailService → EmailHelper クラスへ変更します。
Project View で src/main/java/ksbysample/webapp/lending/service の下の EmailService.java を選択してから F6 を押して「Move」ダイアログを表示します。移動先のパッケージに ksbysample.webapp.lending.helper.mail を指定した後「OK」ボタンをクリックします。
src/main/java/ksbysample/webapp/lending/helper/mail の下の EmailService.java を選択してから Shift+F6 を押して「Rename」ダイアログを表示します。クラス名を EmailHelper に変更した後「OK」ボタンをクリックします。
EmailService クラスはテストも作成しているため「Rename Tests」ダイアログが表示されます。「Select all」ボタンをクリックした後「OK」ボタンをクリックします。
「Rename Variables」ダイアログも表示されますので、「Select all」ボタンをクリックした後「OK」ボタンをクリックします。
作成済のテストが通るか確認します。最初に clean タスクの実行→「Rebuild Project」メニューの実行→build タスクの実行を行い、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。
build タスクを実行したところテストが1つエラーになりました。
Project View のルートでコンテキストメニューを表示して「Run 'Tests in 'ksbysample...' with Coverage」を選択してテストを実行してみます。こちらもテストが1つ失敗しており、原因は下記の画像のテストで HTTP ステータスコードの 403 が返るべきところ 200 が返ってきているためでした。原因を調査します。
bootRun で Tomcat を起動して画面から動作を確認したり、1.0.x のブランチに切り戻して動作を確認した結果、以下の原因であることが分かりました。
- まず suzuki hanako ( user_id = 2 ) の ID の有効期限が
2015-09-30 22:19:02.783000
で期限切れでした。 - また有効期限切れを修正しても ExceptionHandlerAdvice.java の実装前であれば 403 ( Forbidden ) が返るのですが、実装後は 200 が返るようになっていました。
よって以下の修正をします。
- まず suzuki hanako ( user_id = 2 ) の ID の有効期限が
src/test/java/resources/testdata/base の下の user_info.csv を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/web の下の ExceptionHandlerAdvice.java を リンク先の内容 に変更します。
再度、作成済のテストが通るか確認します。clean タスクの実行→「Rebuild Project」メニューの実行→build タスクの実行を行います。
"BUILD SUCCESSFUL" のメッセージが出力されることが確認できます。
Project View のルートでコンテキストメニューを表示して「Run 'Tests in 'ksbysample...' with Coverage」を選択してテストを実行してみます。
テストが実行され、全て成功することが確認できます。
Velocity のテンプレートファイルの作成
メール生成用ヘルパークラスの作成
- src/main/java/ksbysample/webapp/lending/helper/mail の下に Mail002Helper.java を作成します。作成後、リンク先の内容 に変更します。
ユーザ情報用ヘルパークラスの作成
ユーザ情報用ヘルパークラスを作成し、承認者のメールアドレス一覧を返すメソッドを実装します。
src/main/java/ksbysample/webapp/lending/dao の下の UserInfoDao.java を リンク先の内容 に変更します。
src/main/resources/META-INF/ksbysample/webapp/lending/dao/UserInfoDao の下に selectApproverMailAddrList.sql を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/helper の下に user パッケージを作成します。
src/main/java/ksbysample/webapp/lending/helper/user の下に UserHelper.java を作成します。作成後、リンク先の内容 に変更します。
LendingappService クラスの変更
LendingappController クラスの変更
- src/main/java/ksbysample/webapp/lending/web/lendingapp の下の LendingappController.java を リンク先の内容 に変更します。
動作確認
動作確認します。データは Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その31 )( 貸出申請画面の作成2 ) で作成した貸出申請ID = 105 のデータを使用します。lending_app.status の値が 3 になっている場合には 2 に変更しておきます。
メールを受信するので smtp4dev が起動していない場合には起動します。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動し http://localhost:8080/lendingapp?lendingAppId=105 へアクセスします。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。
貸出申請画面が表示されたら何件かの書籍で「申請する」を選択して申請理由を記入した後、「申請」ボタンをクリックします。
ROLE_APPROVER 権限を付与されている tanaka.taro@sample.com へメールが送信されることが確認できます。
Ctrl+F2 を押して Tomcat を停止します。
一旦 commit します。
ソースコード
user_info.csv
user_id,username,password,mail_address,enabled,cnt_badcredentials,expired_account,expired_password 1,"tanaka taro",$2a$10$LKKepbcPCiT82NxSIdzJr.9ph.786Mxvr.VoXFl4hNcaaAn9u7jje,tanaka.taro@sample.com,1,0,"9999-12-31 23:59:00.000000","9999-12-31 23:59:00.000000" 2,"suzuki hanako",$2a$10$.fiPEZ155Rl41/e.mdM3A.mG0iEQNPmhjFL/aIiV8dZnXsCd.oqji,suzuki.hanako@test.co.jp,1,0,"9999-12-31 23:59:00.000000","9999-12-31 23:59:00.000000" 3,"kimura masao",$2a$10$yP1dLPIq9j7WQVH6ruSwkepf8jIkPxTtncbSnYM0/jAGQ4HCQO8R.,kimura.masao@test.co.jp,0,0,"2015-12-31 22:30:54.425000","2015-10-15 22:31:03.316000" 4,"endo yoko",$2a$10$PVFe8Lh1Pkjc54DWS9mJL.q407x51ZK8MSXhwuTF9zxCnnt80LKwy,endo.yoko@sample.com,1,0,"2015-01-10 22:31:55.454000","2015-12-31 22:32:11.886000" 5,"sato masahiko",$2a$10$qIU0kM/p1pa7KSIjF6YA4eORd2wL1Eo6TlvH./DmPs7D.xXQPEq7a,sato.masahiko@sample.com,1,0,"2015-12-31 22:34:14.827000","2014-08-05 22:34:22.818000" 6,"takahasi naoko",$2a$10$iXp/d4wXmfaLKTjQKBvik.kETgx4nQ.FL1NjYt4ALJOGSyVOSchW6,takahasi.naoko@test.co.jp,1,0,"2015-12-01 22:39:48.475000","2015-11-10 22:39:55.422000" 7,"kato hiroshi",$2a$10$g5dtFTtNBdJO30aHg50rluGNa2pEAzArcwYkYyCG91ElBZPs9sDi2,kato.hiroshi@sample.com,0,5,"2014-01-01 15:58:53.295000","2013-12-31 15:59:07.668000"
- user_id = 1, 2 の有効期限を
9999-12-31 23:59:00.000000
へ変更します。
ExceptionHandlerAdvice.java
package ksbysample.webapp.lending.web; import com.google.common.base.Joiner; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.ModelAndView; import javax.servlet.RequestDispatcher; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @ControllerAdvice public class ExceptionHandlerAdvice { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @ExceptionHandler(Exception.class) public ModelAndView handleException(Exception e , HttpServletRequest request , HttpServletResponse response) throws IOException { String url; if (StringUtils.equals(request.getRequestURI(), "/error")) { url = (String) request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI); } else { url = request.getRequestURL().toString(); } url += (StringUtils.isNotEmpty(request.getQueryString()) ? "?" + request.getQueryString() : ""); logger.error("URL = {}", url, e); ModelAndView model = new ModelAndView("error"); List<String> errorInfoList = new ArrayList<>(); // エラーメッセージ if (e != null && StringUtils.isNotEmpty(e.getMessage())) { model.addObject("errorMessage", e.getMessage()); // Spring Security の機能でアクセス不可と判断された場合に HTTPステータスコードが 403 になるようにする if (e instanceof org.springframework.security.access.AccessDeniedException) { response.setStatus(HttpStatus.FORBIDDEN.value()); } } else { model.addObject("errorMessage", response.getStatus() + " " + HttpStatus.valueOf(response.getStatus()).getReasonPhrase()); } // エラー発生日時 model.addObject("currentdt", LocalDateTime.now()); // URL errorInfoList.add(" "); errorInfoList.add("エラーが発生したURL: " + url); // URLパラメータ errorInfoList.add("URLパラメータ一覧:"); Map<String, String[]> params = request.getParameterMap(); params.entrySet().stream() .forEach(param -> errorInfoList.add(param.getKey() + ":" + Joiner.on(", ").join(param.getValue()))); model.addObject("errorInfoList", errorInfoList); // スタックトレース try ( StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw) ) { e.printStackTrace(pw); pw.flush(); String stacktrace = sw.toString(); errorInfoList.add(" "); Arrays.asList(stacktrace.split(System.lineSeparator())).stream() .forEach(line -> errorInfoList.add(line)); }; return model; } }
if (e instanceof org.springframework.security.access.AccessDeniedException) { ... }
を追加します。
mail002-body.vm
貸出申請がありました。以下のURLから申請内容を確認してください。 http://localhost:8080/lendingapproval?lendingAppId=${lendingAppId}
Mail002Helper.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 Mail002Helper { private final String TEMPLATE_LOCATION_TEXTMAIL = "mail/mail002-body.vm"; private final String FROM_ADDR = "lendingapp@sample.com"; private final String SUBJECT = "貸出申請がありました"; @Autowired private VelocityUtils velocityUtils; @Autowired private JavaMailSender mailSender; public MimeMessage createMessage(String[] toAddrList, Long lendingAppId) throws MessagingException { MimeMessage mimeMessage = this.mailSender.createMimeMessage(); MimeMessageHelper message = new MimeMessageHelper(mimeMessage, false, "UTF-8"); message.setFrom(FROM_ADDR); message.setTo(toAddrList); 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); } }
UserInfoDao.java
package ksbysample.webapp.lending.dao; import ksbysample.webapp.lending.entity.UserInfo; 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 UserInfoDao { /** * @param userId * @return the UserInfo entity */ @Select UserInfo selectById(Long userId); @Select UserInfo selectByMailAddress(String mailAddress); @Select List<String> selectApproverMailAddrList(); /** * @param entity * @return affected rows */ @Insert int insert(UserInfo entity); /** * @param entity * @return affected rows */ @Update int update(UserInfo entity); @Update(sqlFile = true) int incCntBadcredentialsByMailAddress(String mailAddress); @Update(sqlFile = true) int initCntBadcredentialsByMailAddress(String mailAddress); /** * @param entity * @return affected rows */ @Delete int delete(UserInfo entity); }
List<String> selectApproverMailAddrList();
を追加します。
selectApproverMailAddrList.sql
select ui.mail_address from user_role ur , user_info ui where ur.role = 'ROLE_APPROVER' and ui.user_id = ur.user_id
UserHelper.java
package ksbysample.webapp.lending.helper.user; import com.google.common.collect.Iterables; import ksbysample.webapp.lending.dao.UserInfoDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.List; @Component public class UserHelper { @Autowired private UserInfoDao userInfoDao; public String[] getApprovalMailAddrList() { List<String> approvalMailAddrList = userInfoDao.selectApproverMailAddrList(); return Iterables.toArray(approvalMailAddrList, String.class); } }
LendingappService.java
package ksbysample.webapp.lending.web.lendingapp; 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.exception.WebApplicationRuntimeException; import ksbysample.webapp.lending.helper.mail.EmailHelper; import ksbysample.webapp.lending.helper.mail.Mail002Helper; import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper; import ksbysample.webapp.lending.helper.user.UserHelper; import org.apache.commons.lang3.StringUtils; import org.seasar.doma.jdbc.SelectOptions; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.util.List; import static ksbysample.webapp.lending.values.LendingAppStatusValues.PENDING; import static ksbysample.webapp.lending.values.LendingBookLendingAppFlgValues.APPLY; @Service public class LendingappService { @Autowired private LendingAppDao lendingAppDao; @Autowired private LendingBookDao lendingBookDao; @Autowired private MessagesPropertiesHelper messagesPropertiesHelper; @Autowired private EmailHelper emailHelper; @Autowired private Mail002Helper mail002Helper; @Autowired private UserHelper userHelper; public LendingApp getLendingApp(Long lendingAppId) { LendingApp lendingApp = lendingAppDao.selectById(lendingAppId); if (lendingApp == null) { throw new WebApplicationRuntimeException( messagesPropertiesHelper.getMessage("LendingappForm.lendingApp.nodataerr", null)); } return lendingApp; } public List<LendingBook> getLendingBookList(Long lendingAppId) { List<LendingBook> lendingBookList = lendingBookDao.selectByLendingAppId(lendingAppId); return lendingBookList; } public void apply(LendingappForm lendingappForm) throws MessagingException { // 更新対象のデータを取得する(ロックする) LendingApp lendingApp = lendingAppDao.selectById(lendingappForm.getLendingApp().getLendingAppId() , SelectOptions.get().forUpdate()); List<LendingBook> lendingBookList = lendingBookDao.selectByLendingAppId(lendingappForm.getLendingApp().getLendingAppId() , SelectOptions.get().forUpdate()); // lending_app.status を 3(申請中) にする lendingApp.setStatus(PENDING.getValue()); lendingAppDao.update(lendingApp); // lending_book.lending_app_flg を 1(する) に、lending_app_reason に画面に入力された申請理由をセットする lendingappForm.getLendingBookDtoList().stream() .filter(lendingBookDto -> StringUtils.equals(lendingBookDto.getLendingAppFlg(), APPLY.getValue())) .forEach(lendingBookDto -> { LendingBook lendingBook = new LendingBook(); BeanUtils.copyProperties(lendingBookDto, lendingBook); lendingBookDao.updateLendingAppFlgAndReason(lendingBook); }); // 承認者にメールを送信する String[] approverMailAddrList = userHelper.getApprovalMailAddrList(); MimeMessage mimeMessage = mail002Helper.createMessage(approverMailAddrList, lendingApp.getLendingAppId()); emailHelper.sendMail(mimeMessage); } public void temporarySave(LendingappForm lendingappForm) { // 更新対象のデータを取得する(ロックする) LendingApp lendingApp = lendingAppDao.selectById(lendingappForm.getLendingApp().getLendingAppId() , SelectOptions.get().forUpdate()); List<LendingBook> lendingBookList = lendingBookDao.selectByLendingAppId(lendingappForm.getLendingApp().getLendingAppId() , SelectOptions.get().forUpdate()); // lending_book.lending_app_flg, lending_app_reason に画面に入力された内容をセットする lendingappForm.getLendingBookDtoList().stream() .forEach(lendingBookDto -> { LendingBook lendingBook = new LendingBook(); BeanUtils.copyProperties(lendingBookDto, lendingBook); lendingBookDao.updateLendingAppFlgAndReason(lendingBook); }); } }
- 以下の3つのフィールドを追加します。
private EmailHelper emailHelper;
private Mail002Helper mail002Helper
private UserHelper userHelper;
- apply メソッドの以下の点を変更します。
throws MessagingException
を追加します。- 最後に承認者にメールを送信する処理を追加します。
LendingappController.java
package ksbysample.webapp.lending.web.lendingapp; import ksbysample.webapp.lending.cookie.CookieLastLendingAppId; import ksbysample.webapp.lending.entity.LendingApp; import ksbysample.webapp.lending.entity.LendingBook; import ksbysample.webapp.lending.exception.WebApplicationRuntimeException; import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper; import ksbysample.webapp.lending.helper.thymeleaf.SuccessMessagesHelper; import ksbysample.webapp.lending.util.cookie.CookieUtils; import org.apache.commons.lang.StringUtils; 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 org.springframework.web.bind.annotation.RequestMethod; import javax.mail.MessagingException; import javax.servlet.http.HttpServletResponse; import java.util.List; import static ksbysample.webapp.lending.values.LendingAppStatusValues.UNAPPLIED; @Controller @RequestMapping("/lendingapp") public class LendingappController { @Autowired private LendingappService lendingappService; @Autowired private MessagesPropertiesHelper messagesPropertiesHelper; @Autowired private LendingappFormValidator lendingappFormValidator; @InitBinder(value = "lendingappForm") public void initBinder(WebDataBinder binder) { binder.addValidators(lendingappFormValidator); } @RequestMapping public String index(@Validated LendingappParamForm lendingappParamForm , BindingResult bindingResultForLendingappParamForm , LendingappForm lendingappForm , HttpServletResponse response) { if (bindingResultForLendingappParamForm.hasErrors()) { throw new WebApplicationRuntimeException( messagesPropertiesHelper.getMessage("LendingappForm.lendingAppId.emptyerr", null)); } // 画面に表示するデータを取得する setDispData(lendingappParamForm.getLendingAppId(), lendingappForm); // 未申請の場合には LastLendingAppId Cookie に貸出申請ID をセットする if (StringUtils.equals(lendingappForm.getLendingApp().getStatus(), UNAPPLIED.getValue())) { CookieUtils.addCookie(CookieLastLendingAppId.class , response, String.valueOf(lendingappParamForm.getLendingAppId())); } return "lendingapp/lendingapp"; } @RequestMapping(value = "/apply", method = RequestMethod.POST) public String apply(@Validated LendingappForm lendingappForm , BindingResult bindingResult , HttpServletResponse response) throws MessagingException { if (bindingResult.hasErrors()) { return "lendingapp/lendingapp"; } // 入力された内容で申請する lendingappService.apply(lendingappForm); // 画面に表示するデータを取得する setDispData(lendingappForm.getLendingApp().getLendingAppId(), lendingappForm); // LastLendingAppId Cookie を削除する CookieUtils.removeCookie(CookieLastLendingAppId.class, response); return "lendingapp/lendingapp"; } @RequestMapping(value = "/temporarySave", method = RequestMethod.POST) public String temporarySave(@Validated LendingappForm lendingappForm , BindingResult bindingResult , Model model) { if (bindingResult.hasErrors()) { return "lendingapp/lendingapp"; } // 入力された内容を一時保存する lendingappService.temporarySave(lendingappForm); // 画面に表示する通常メッセージをセットする SuccessMessagesHelper successMessagesHelper = new SuccessMessagesHelper("一時保存しました"); successMessagesHelper.setToModel(model); return "lendingapp/lendingapp"; } private void setDispData(Long lendingAppId, LendingappForm lendingappForm) { LendingApp lendingApp = lendingappService.getLendingApp(lendingAppId); List<LendingBook> lendingBookList = lendingappService.getLendingBookList(lendingAppId); lendingappForm.setLendingApp(lendingApp); lendingappForm.setLendingBookList(lendingBookList); } }
- apply メソッドに
throws MessagingException
を追加します。
履歴
2015/12/20
初版発行。