Spring Boot でメール送信する Web アプリケーションを作る ( その18 )( 送信済メール検索画面の作成 )
概要
Spring Boot でメール送信する Web アプリケーションを作る ( その17 )( モックツール JMockit を試す ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 送信済メール検索画面の作成
- 今回は検索処理まで実装し、ページネーションは実装しません。
ソフトウェア一覧
参考にしたサイト
プログラム の個人的なメモ - 【JUnit】JUnit / TestWatcher
http://blogs.yahoo.co.jp/dk521123/34933920.html- テスト実行前にテストデータを投入する仕組みを作成する時に、JUnit の TestWatcher の使い方を参考にしました。
アノテーション作成方法
http://www.ne.jp/asahi/hishidama/home/tech/java/annotation.htmlDoma 2 - User Documentation - 式言語
http://doma.readthedocs.org/ja/latest/expression/- 「組み込み関数の使用」のところに、like 検索時に前方一致検索や中間一致検索で使用する関数が記述されています。
- 今回検索画面の SQL ファイルでは中間一致検索の @infix を使用します。
手順
ブランチの作成
- IntelliJ IDEA で 1.0.x-make-mailsearch ブランチを作成します。
Form クラスの作成
まずは Form クラスの作成と Thymeleaf テンプレートファイルの修正を行い、画面が表示されるところまで実装します。
- src/main/java/ksbysample/webapp/email/web/mailsearch の下に MailsearchForm.java を新規作成します。作成後、リンク先の内容 に変更します。
Thymeleaf テンプレートファイルの修正
- src/main/resources/templates/mailsearch の下の mailsearch.html を リンク先のその1の内容 に変更します。
Controller クラスの修正
- src/main/java/ksbysample/webapp/email/web/mailsearch の下の MailsearchController.java を リンク先のその1の内容 に変更します。
動作確認
画面が正常に表示されることを確認します。Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動し http://localhost:8080/mailsearch へアクセスします。画面が正常に表示されることを確認します。
Run View で Ctrl+F2 を押して Tomcat を停止します。
テストデータ投入用クラスの作成
入力された検索条件で select 文を実行し、結果を表示する処理を実装します。
最初に ksbysample.webapp.email.test.TestDataResource とは別にテストデータを投入する仕組みを作成します。以下の仕様です。
- テストクラスに @Rule アノテーションを付加した TestDataLoaderResource クラスのフィールドを追加します。
- テスト開始前にテストデータを投入したいテストメソッドに TestDataLoader アノテーションを付加し、テストデータの CSV ファイルを配置したディレクトリを指定します。
- TestDataLoaderResource クラスは TestWatcher クラスを継承して実装します。テスト開始前に TestDataLoaderResource クラスの starting メソッドが呼び出され、TestDataLoader アノテーションに記述されたディレクトリ内にある CSV ファイルをテーブルに読み込みます。
src/test/java/ksbysample/webapp/email/test の下に TestDataLoader.java を新規作成します。作成後、リンク先の内容 に変更します。
src/test/java/ksbysample/webapp/email/test の下に TestDataLoaderResource.java を新規作成します。作成後、リンク先の内容 に変更します。
MailsearchDao クラスと SQL ファイルの作成
src/main/java/ksbysample/webapp/email/web/mailsearch の下に MailsearchDao.java を新規作成します。作成後、リンク先の内容 に変更します。
src/main/resources/META-INF/ksbysample/webapp/email/web/mailsearch/MailsearchDao の下に selectCondition.sql を新規作成します。作成後、リンク先の内容 に変更します。
テストで使用するテストデータを作成します。src/test/resources/ksbysample/webapp/email/web の下に mailsearch/testdata ディレクトリを新規作成します。
src/test/resources/ksbysample/webapp/email/web/mailsearch/testdata の下に email.csv, table-ordering.txt を新規作成します。作成後、リンク先の内容 に変更します。
テストを作成します。src/test/java/ksbysample/webapp/email/web/mailsearch の下に MailsearchDaoTest.java を新規作成します。作成後、リンク先の内容 に変更します。
テストを実行します。SQL 文が正常に生成されているか確認したいので、テスト実行時の spring.profiles.active の設定を develop に変更します。
IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択します。
「Run/Debug Configurations」ダイアログが表示されます。画面左側のツリーから「Defaults」-「JUnit」を選択した後、画面右側の「VM options」に設定されている文字列を
-Dspring.profiles.active=unittest
→-Dspring.profiles.active=develop
へ変更して「OK」ボタンをクリックします。MailsearchDaoTest クラス内の testSelectCondition メソッドにカーソルを移動した後、コンテキストメニューを表示して「Run 'testSelectCondition' with Coverage」メニューを選択します。
テストが実行されて成功しますが、SQL 文の日本語の部分が文字化けしていました。
文字化けしないようにします。src/main/resources の下の logback.xml を リンク先の内容 に変更します。
再度テストを実行します。今度は文字化けせずに SQL 文が出力されました。
また、生成された SQL 文も以下の点が対応できていることが確認できます。
- select の後に email テーブルのカラムが展開されています。また各カラム名に "em." が付いています。
- mailsearchForm に値をセットしたもののみ where 句に条件が出力されています。
- type 以外は mailsearchForm にセットした値の前後に "%" が付いて、部分一致検索になっています。
「Run/Debug Configurations」ダイアログの「Defaults」-「JUnit」の「VM options」の設定を
-Dspring.profiles.active=unittest
に戻しておきます。
Controller クラス、Thymeleaf テンプレートファイルの修正
src/main/java/ksbysample/webapp/email/web/mailsearch の下の MailsearchController.java を リンク先のその2の内容 に変更します。
src/main/resources/templates/mailsearch の下の mailsearch.html を リンク先のその2の内容 に変更します。
動作確認
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動し http://localhost:8080/mailsearch へアクセスします。email テーブルのデータが全て一覧に表示されていることを確認します。
検索条件を入力して「検索」ボタンをクリックし、入力した検索条件にヒットするデータだけが表示されることを確認します。
Run View で Ctrl+F2 を押して Tomcat を停止します。
commit、Push、Pull Request、マージ
Project View のルートでコンテキストメニューを表示して「Run 'Tests in 'ksbysample...' with Coverage」を選択し、テストが全て成功することを確認します。
Gradle projects View から build タスクを実行し、"BUILD SUCCESSFUL" が出力されることを確認します。
commit します。
GitHub へ Push、1.0.x-make-mailsearch -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-make-mailsearch ブランチを削除、をします。
次回は。。。
送信済メール検索画面のページネーションを実装します。
ソースコード
MailsearchForm.java
package ksbysample.webapp.email.web.mailsearch; import lombok.Data; import java.util.List; @Data public class MailsearchForm { private String toAddr; private String subject; private String name; private List<String> type; }
mailsearch.html
■その1
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <title>ksbysample-webapp-email</title> <meta content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' name='viewport'/> <meta th:replace="common/head-cssjs"/> <style> <!-- .form-group { margin-bottom: 5px; } .table { margin-top: 10px; margin-bottom: 0px; } .table>tbody>tr>td , .table>tbody>tr>th , .table>tfoot>tr>td , .table>tfoot>tr>th , .table>thead>tr>td , .table>thead>tr>th { padding: 5px; } .checkbox label, .radio label { padding-right: 10px; } --> </style> </head> <body class="skin-blue"> <div class="wrapper"> <!-- Main Header --> <div th:replace="common/mainparts :: main-header"></div> <!-- Left side column. contains the logo and sidebar --> <div th:replace="common/mainparts :: main-sidebar (active='mailsearch')"></div> <!-- Content Wrapper. Contains page content --> <div class="content-wrapper"> <!-- Content Header (Page header) --> <section class="content-header"> <h1> 送信済メール検索画面 </h1> </section> <!-- Main content --> <section class="content"> <div class="row"> <div class="col-xs-12"> <div class="box"> <div class="box-header with-border bg-purple-gradient"> <div class="row"> <div class="col-xs-12"> <form id="mailSearchForm" method="post" action="/mailsearch" th:action="@{/mailsearch}" th:object="${mailsearchForm}" class="form-horizontal"> <div class="form-group"> <label for="toAddr" class="control-label col-sm-2">To</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-8"><div class="input-group"><span class="input-group-addon"><i class="fa fa-envelope"></i></span><input type="text" name="toAddr" id="toAddr" class="form-control input-sm" value="" placeholder="" th:field="*{toAddr}"/></div></div></div> </div> </div> <div class="form-group"> <label for="subject" class="control-label col-sm-2">Subject</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-12"><input type="text" name="subject" id="subject" class="form-control input-sm" value="" placeholder="" th:field="*{subject}"/></div></div> </div> </div> <div class="form-group"> <label for="name" class="control-label col-sm-2">氏名</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-8"><input type="text" name="name" id="name" class="form-control input-sm" value="" placeholder="" th:field="*{name}"/></div></div> </div> </div> <div class="form-group"> <label class="control-label col-sm-2">項目</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-12"> <div class="checkbox"> <label th:each="type : ${T(ksbysample.webapp.email.config.Constant).getInstance().TYPE_MAP.entrySet()}"> <input type="checkbox" name="type" th:value="${type.getKey()}" th:text="${type.getValue()}" th:field="*{type}"/> </label> </div> </div></div> </div> </div> <div class="text-center"> <button type="button" id="search" value="search" class="btn btn-primary bg-gray">検索</button> </div> </form> </div> </div> </div> <div class="box-body"> <div id="maillist_wrapper" class="dataTables_wrapper form-inline" role="grid"> <table id="maillist" class="table table-bordered table-hover dataTable" aria-describedby="maillist_info"> <thead class="bg-purple"> <tr role="row"> <th role="columnheader" tabindex="0" aria-controls="maillist" rowspan="1" colspan="1">To</th> <th role="columnheader" tabindex="0" aria-controls="maillist" rowspan="1" colspan="1">Subject</th> <th role="columnheader" tabindex="0" aria-controls="maillist" rowspan="1" colspan="1">氏名</th> <th role="columnheader" tabindex="0" aria-controls="maillist" rowspan="1" colspan="1">項目</th> </tr> </thead> <tbody role="alert" aria-live="polite" aria-relevant="all"> <tr> <td>test@sample.com</td> <td>件名1</td> <td>田中 太郎</td> <td>資料請求</td> </tr> <tr> <td>test2@sample.com</td> <td>件名2</td> <td>鈴木 花子</td> <td>商品に関する苦情</td> </tr> <tr> <td>test3@sample.com</td> <td>件名3</td> <td>木村 二郎</td> <td>その他</td> </tr> </tbody> </table> <div class="row"> <div class="col-xs-12"> <div class="dataTables_paginate paging_bootstrap"> <ul class="pagination"> <li class="prev disabled"><a href="#">← Previous</a></li> <li class="active"><a href="#">1</a></li> <li><a href="#">2</a></li> <li><a href="#">3</a></li> <li><a href="#">4</a></li> <li><a href="#">5</a></li> <li class="next"><a href="#">Next → </a></li> </ul> </div> </div> </div> </div> </div> </div> </div> </div> </section> <!-- /.content --> </div> <!-- /.content-wrapper --> </div> <!-- ./wrapper --> <!-- REQUIRED JS SCRIPTS --> <div th:replace="common/bottom-js"></div> <script type="text/javascript"> <!-- $(document).ready(function() { $('#toAddr').focus(); $('#search').bind('click', function(){ $('#mailSearchForm').submit(); }); }); --> </script> </body> </html>
.checkbox label, .radio label { ... }
を追加します。<form id="mailSearchForm" ... >
にth:object="${mailsearchForm}"
を追加します。- 「To」の name 属性、id 属性を
to
→toAddr
へ変更します。 - 「To」「Subject」「氏名」の input タグに
th:field="*{...}"
を追加します。 - 「項目」の checkbox の html を
<label th:each="type : ...> ... </label>
へ変更します。 $('#to').focus();
→$('#toAddr').focus();
へ変更します。
■その2
<table id="maillist" class="table table-bordered table-hover dataTable" aria-describedby="maillist_info"> <thead class="bg-purple"> <tr role="row"> <th role="columnheader" tabindex="0" aria-controls="maillist" rowspan="1" colspan="1">To</th> <th role="columnheader" tabindex="0" aria-controls="maillist" rowspan="1" colspan="1">Subject</th> <th role="columnheader" tabindex="0" aria-controls="maillist" rowspan="1" colspan="1">氏名</th> <th role="columnheader" tabindex="0" aria-controls="maillist" rowspan="1" colspan="1">項目</th> </tr> </thead> <tbody role="alert" aria-live="polite" aria-relevant="all" th:each="email : ${emailList}"> <tr> <td th:text="${email.toAddr}">test@sample.com</td> <td th:text="${email.subject}">件名1</td> <td th:text="${email.name}">田中 太郎</td> <td th:text="${T(ksbysample.webapp.email.config.Constant).getInstance().TYPE_MAP.get(email.type)}">資料請求</td> </tr> </tbody> </table>
- tbody タグに
th:each="email : ${emailList}"
を追加します。 - tbody 内の td タグに
th:text="${...}"
を追加します。「項目」はth:text="${email.type}"
のままだとコードが表示されてしまうので、th:text="${T(ksbysample.webapp.email.config.Constant).getInstance().TYPE_MAP.get(email.type)}"
と記述してコードに対応する文字列に変換します。
MailsearchController.java
■その1
package ksbysample.webapp.email.web.mailsearch; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/mailsearch") public class MailsearchController { @RequestMapping public String index(MailsearchForm mailsearchForm) { return "mailsearch/mailsearch"; } }
- index メソッドの引数に
MailsearchForm mailsearchForm
を追加します。
■その2
package ksbysample.webapp.email.web.mailsearch; import ksbysample.webapp.email.entity.Email; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import java.util.List; @Controller @RequestMapping("/mailsearch") public class MailsearchController { @Autowired private MailsearchDao mailsearchDao; @RequestMapping public String index(MailsearchForm mailsearchForm, Model model) { List<Email> emailList = mailsearchDao.selectCondition(mailsearchForm); model.addAttribute("emailList", emailList); return "mailsearch/mailsearch"; } }
private MailsearchDao mailsearchDao;
を追加します。- index メソッドの引数に
Model model
を追加します。 - index メソッド内の処理に mailsearchDao.selectCondition を呼び出して検索し、model.addAttribute("emailList", emailList); する処理を追加します。
TestDataLoader.java
package ksbysample.webapp.email.test; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface TestDataLoader { String value(); }
TestDataLoaderResource.java
package ksbysample.webapp.email.test; import org.dbunit.database.DatabaseConnection; import org.dbunit.database.IDatabaseConnection; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.ReplacementDataSet; import org.dbunit.dataset.csv.CsvDataSet; import org.dbunit.operation.DatabaseOperation; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.io.File; import java.lang.annotation.Annotation; import java.util.Collection; @Component public class TestDataLoaderResource extends TestWatcher { @Autowired private DataSource dataSource; @Override protected void starting(Description description) { try { IDatabaseConnection conn = null; try { conn = new DatabaseConnection(dataSource.getConnection()); Collection<Annotation> annotationList = description.getAnnotations(); for (Annotation annotation : annotationList) { if (annotation instanceof TestDataLoader) { TestDataLoader testDataLoader = (TestDataLoader)annotation; IDataSet dataSet = new CsvDataSet(new File(testDataLoader.value())); ReplacementDataSet replacementDataset = new ReplacementDataSet(dataSet); replacementDataset.addReplacementObject("[null]", null); DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDataset); } } } finally { if (conn != null) conn.close(); } } catch (Exception e) { e.printStackTrace(); } } }
- テスト実行前に呼び出される TestWatcher の starting メソッドを利用します。
- starting メソッドの引数 description のメソッド getAnnotations() でテストメソッドのアノテーション一覧が取得できますので、@TestDataLoader アノテーションが付加されているか確認します。
- @TestDataLoader アノテーションが付加されている場合には、CSV ファイルのデータをテーブルへロードします。
MailsearchDao.java
package ksbysample.webapp.email.web.mailsearch; import ksbysample.webapp.email.annotation.dao.ComponentAndAutowiredDomaConfig; import ksbysample.webapp.email.entity.Email; import org.seasar.doma.Dao; import org.seasar.doma.Select; import java.util.List; @Dao @ComponentAndAutowiredDomaConfig public interface MailsearchDao { @Select List<Email> selectCondition(MailsearchForm mailsearchForm); }
selectCondition.sql
select /*%expand "em" */* from email em where /*%if mailsearchForm.toAddr != null */ em.to_addr like /* @infix(mailsearchForm.toAddr) */'%t%' /*%end*/ /*%if mailsearchForm.subject != null */ and em.subject like /* @infix(mailsearchForm.subject) */'%スト%' /*%end*/ /*%if mailsearchForm.name != null */ and em.name like /* @infix(mailsearchForm.name) */'%花%' /*%end*/ /*%if mailsearchForm.type != null */ and em.type in /* mailsearchForm.type */('1', '3') /*%end*/ order by em.to_addr
email.csv, table-ordering.txt
■email.csv
email_id,from_addr,to_addr,subject,name,sex,type,naiyo 1503,test@sample.com,xxx@yyy.zzz,テスト,"田中 太郎",1,1,これはテストですか? 1504,test@sample.com,xxx@yyy.zzz,テスト,"田中 太郎",1,1,これはテストです。 1505,tanaka@sample.com,test@sample.com,テスト2,"木村 花子",2,3,これもテストですか? 1506,test@sample.com,tanaka@yyy.zzz,テスト,"田中 太郎",1,1,これはテストです。 1507,test@sample.com,hanako@sample.com,テスト2,"鈴木 花子",2,2,これもテストですか?
- データをテーブルに登録後、IntelliJ IDEA Ulitimate Edition の Database Tools の機能で作成しました。以下の手順で出力しています。
テーブルのカラム名を出力する設定に変更します。Database View を開き email テーブルを選択してコンテキストメニューを表示後、「Save To File」-「Configure Extractors...」を選択します。
「Data Extractors」ダイアログが表示されます。画面左側のリストから「Comma-separated Values(CSV)」を選択した後、画面右側の「Include column names」チェックボックスをチェックして「OK」ボタンをクリックします。
再度 Database View で email テーブルを選択してコンテキストメニューを表示後、「Save To File」-「Comma-separated Values(CSV)」を選択します。
「Save Data To File」ダイアログが表示されます。出力先に src/test/resources/ksbysample/webapp/email/web/mailsearch/testdata を選択し、ファイル名を email.csv に変更して「OK」ボタンをクリックします。
■table-ordering.txt
MailsearchDaoTest.java
package ksbysample.webapp.email.web.mailsearch; import ksbysample.webapp.email.Application; import ksbysample.webapp.email.entity.Email; import ksbysample.webapp.email.test.TestDataLoader; import ksbysample.webapp.email.test.TestDataLoaderResource; import ksbysample.webapp.email.test.TestDataResource; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class MailsearchDaoTest { @Rule @Autowired public TestDataResource testDataResource; @Rule @Autowired public TestDataLoaderResource testDataLoaderResource; @Autowired private MailsearchDao mailsearchDao; @Test @TestDataLoader("src/test/resources/ksbysample/webapp/email/web/mailsearch/testdata") public void testSelectCondition() throws Exception { MailsearchForm mailsearchForm = new MailsearchForm(); List<Email> emailList = mailsearchDao.selectCondition(mailsearchForm); // emailList.stream().forEach(s -> System.out.println(s.getToAddr())); assertThat(emailList.size(), is(5)); mailsearchForm = new MailsearchForm(); mailsearchForm.setToAddr("t"); mailsearchForm.setSubject("スト"); mailsearchForm.setName("花"); mailsearchForm.setType(Arrays.asList("3")); emailList = mailsearchDao.selectCondition(mailsearchForm); assertThat(emailList.size(), is(1)); mailsearchForm = new MailsearchForm(); mailsearchForm.setName("太郎"); emailList = mailsearchDao.selectCondition(mailsearchForm); assertThat(emailList.size(), is(3)); } }
logback.xml
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${CONSOLE_LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender>
<charset>MS932</charset>
→<charset>UTF-8</charset>
へ変更します。
履歴
2015/06/13
初版発行。