今回も、引き続きチェック処理を含むそのサンプルプログラムについて共有する。前回は、サンプルプログラムの完成イメージについて記載していたが、今回は、具体的なサンプルプログラムのソースコードを共有する。
前提条件
以下の記事の複数画面をもつSpring BootのWEB画面用アプリが作成済であること。
また、以下の記事のプロパティファイルの日本語許可設定が終了していること。今回のサンプルプログラムの完成イメージも、以下の記事を参照のこと。
作成したサンプルプログラムの内容
チェック処理のアノテーションは、「DemoForm.java」の名前・性別・確認チェックに「@NotEmpty」アノテーションを付与している。そのプログラムの内容は以下の通り。
package com.example.demo; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotEmpty; import java.util.LinkedHashMap; import java.util.Map; /** * Formオブジェクトのクラス */ public class DemoForm { /** 名前 */ @Getter @Setter @NotEmpty private String name; /** 生年月日_年 */ @Getter @Setter private String birthYear; /** 生年月日_月 */ @Getter @Setter private String birthMonth; /** 生年月日_日 */ @Getter @Setter private String birthDay; /** 性別 */ @Getter @Setter @NotEmpty private String sex; /** 確認チェック */ @Getter @Setter @NotEmpty private String checked; /** 生年月日_月のMapオブジェクト */ public Map<String,String> getMonthItems(){ Map<String, String> monthMap = new LinkedHashMap<String, String>(); for(int i = 1; i <= 12; i++){ monthMap.put(String.valueOf(i), String.valueOf(i)); } return monthMap; } /** 生年月日_日のMapオブジェクト */ public Map<String,String> getDayItems(){ Map<String, String> dayMap = new LinkedHashMap<String, String>(); for(int i = 1; i <= 31; i++){ dayMap.put(String.valueOf(i), String.valueOf(i)); } return dayMap; } /** 性別のMapオブジェクト */ public Map<String,String> getSexItems(){ Map<String, String> sexMap = new LinkedHashMap<String, String>(); sexMap.put("1", "男"); sexMap.put("2", "女"); return sexMap; } }
また、アノテーションで実現できないチェック処理は、「DemoController.java」のconfirmメソッド, sendメソッド内で実施している。そのプログラムの内容は以下の通り。
package com.example.demo; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.support.SessionStatus; /** * コントローラクラス * 「@SessionAttributes(types = DemoForm.class)」により、 * 生成したFormオブジェクトをセッションとしてもたせている */ @Controller @SessionAttributes(types = DemoForm.class) public class DemoController { /** * Formオブジェクトを初期化して返却する * @return Formオブジェクト */ @ModelAttribute("demoForm") public DemoForm createDemoForm(){ DemoForm demoForm = new DemoForm(); return demoForm; } /** * 入力画面に遷移する * @return 入力画面へのパス */ @GetMapping("/") public String index(){ return "input"; } /** * エラーチェックを行い、エラーが無ければ確認画面に遷移し、 * エラーがあれば入力画面のままとする * @param demoForm Formオブジェクト * @param result バインド結果 * @return 確認画面または入力画面へのパス */ @PostMapping("/confirm") public String confirm(@Validated DemoForm demoForm, BindingResult result){ //formオブジェクトのチェック処理を行う if(result.hasErrors()){ //エラーがある場合は、入力画面のままとする return "input"; } //アノテーション以外のチェック処理を行い、画面遷移する return checkOthers(demoForm, result, "confirm"); } /** * エラーチェックを行い、エラーが無ければ完了画面へのリダイレクトパスに遷移し、 * エラーがあれば入力画面に戻す * @param demoForm Formオブジェクト * @param result バインド結果 * @return 完了画面へのリダイレクトパスまたは入力画面へのパス */ @PostMapping(value = "/send", params = "next") public String send(@Validated DemoForm demoForm, BindingResult result){ //formオブジェクトのチェック処理を行う if(result.hasErrors()){ //エラーがある場合は、入力画面に戻す return "input"; } //アノテーション以外のチェック処理を行い、画面遷移する return checkOthers(demoForm, result, "redirect:/complete"); } /** * 完了画面に遷移する * @param sessionStatus セッションステータス * @return 完了画面 */ @GetMapping("/complete") public String complete(SessionStatus sessionStatus){ //セッションオブジェクトを破棄 sessionStatus.setComplete(); return "complete"; } /** * 入力画面に戻る * @return 入力画面 */ @PostMapping(value = "/send", params = "back") public String back(){ return "input"; } /** * アノテーション以外のチェック処理を行い、画面遷移先を返却 * @param demoForm Formオブジェクト * @param result バインド結果 * @param normalPath 正常時の画面遷移先 * @return 正常時の画面遷移先または入力画面へのパス */ private String checkOthers(DemoForm demoForm, BindingResult result, String normalPath){ //** アノテーション以外のチェック処理を行う //** エラーがある場合は、エラーメッセージ・(エラー時に赤反転するための) //** エラーフィールドの設定を行い、入力画面のままとする //生年月日のチェック処理 int checkDate = DateCheckUtil.checkDate(demoForm.getBirthYear() , demoForm.getBirthMonth(), demoForm.getBirthDay()); switch(checkDate){ case 1: //生年月日_年が空文字の場合のエラー処理 result.rejectValue("birthYear", "validation.date-empty" , new String[]{"生年月日_年"}, ""); return "input"; case 2: //生年月日_月が空文字の場合のエラー処理 result.rejectValue("birthMonth", "validation.date-empty" , new String[]{"生年月日_月"}, ""); return "input"; case 3: //生年月日_日が空文字の場合のエラー処理 result.rejectValue("birthDay", "validation.date-empty" , new String[]{"生年月日_日"}, ""); return "input"; case 4: //生年月日の日付が不正な場合のエラー処理 result.rejectValue("birthYear", "validation.date-invalidate"); //生年月日_月・生年月日_日は、エラーフィールドの設定を行い、 //メッセージを空文字に設定している result.rejectValue("birthMonth", "validation.empty-msg"); result.rejectValue("birthDay", "validation.empty-msg"); return "input"; case 5: //生年月日の日付が未来日の場合のエラー処理 result.rejectValue("birthYear", "validation.date-future"); //生年月日_月・生年月日_日は、エラーフィールドの設定を行い、 //メッセージを空文字に設定している result.rejectValue("birthMonth", "validation.empty-msg"); result.rejectValue("birthDay", "validation.empty-msg"); return "input"; default: //性別が不正に書き換えられていないかチェックする if(!demoForm.getSexItems().keySet().contains(demoForm.getSex())){ result.rejectValue("sex", "validation.sex-invalidate"); return "input"; } //エラーチェックに問題が無いので、正常時の画面遷移先に遷移 return normalPath; } } }
上記「confirm」「send」メソッドでは、まず、DemoForm.javaのアノテーションによるチェックを行えるよう、引数のdemoFormに「@Validated」アノテーションを付与している。また、アノテーション以外のチェック処理は「checkOthers」メソッドにて実施している。
エラーの場合は、BindingResultクラスのrejectValueメソッドで、エラーメッセージと(エラー時に赤反転するための)エラ-フィールドの設定を行っている。さらに、エラーの場合と正常な場合で、画面遷移先を変更している。
なお、確認画面でもチェック処理を行っているのは、デベロッパーツールで裏からデータを書き換えられた場合に備えるためである。
エラーメッセージは、「messages.properties」にて実施している。その内容は以下の通りで、独自の日付チェックのメッセージと、DemoForm.javaのアノテーションのメッセージ(javax.validation.constraints.NotEmpty.message)の両方を含んでいる。さらに、エラーメッセージ{0}に埋め込むフィールド名も、ここで定義している。
#メッセージ validation.date-empty={0}を入力してください。 validation.date-invalidate=生年月日が存在しない日付になっています。 validation.date-future=生年月日が未来の日付になっています。 validation.empty-msg= javax.validation.constraints.NotEmpty.message={0}を入力してください。 validation.sex-invalidate=性別に不正な値が入っています。 #フィールド名 name=名前 sex=性別 checked=確認チェック
DemoForm.javaのアノテーションのメッセージは、通常、Spring Bootのデフォルトの「ValidationMessages.properties」で定義するが、この定義を「application.properties」に変更することもできる。その変更は、下記「DemoApplication.java」にて実施している。そのプログラムの内容は以下の通り。
package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @SpringBootApplication public class DemoApplication implements WebMvcConfigurer { @Autowired private MessageSource messageSource; public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Bean public LocalValidatorFactoryBean validator() { //Spring Bootデフォルトのエラーメッセージのプロパティファイルを //ValidationMessages.propertiesからmessages.propertiesに変更する LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean(); localValidatorFactoryBean.setValidationMessageSource(messageSource); return localValidatorFactoryBean; } @Override public org.springframework.validation.Validator getValidator() { return validator(); } }
さらに、日付チェック処理は、下記「DateCheckUtil.java」にて実施している。これは前出の「DemoController.java」内のcheckDateメソッドから呼び出される。
package com.example.demo; import java.time.LocalDate; import java.time.chrono.JapaneseChronology; import java.time.format.DateTimeFormatter; import java.time.format.ResolverStyle; import java.util.Locale; public class DateCheckUtil { /** * 日付チェック処理を行う * @param year 年 * @param month 月 * @param day 日 * @return 判定結果(1:年が空、2:月が空、3:日が空、 * 4:年月日が不正、5:年月日が未来日、0:正常) */ public static int checkDate(String year, String month, String day){ final String dateFormat = "uuuuMMdd"; if(isEmpty(year)){ return 1; } if(isEmpty(month)){ return 2; } if(isEmpty(day)){ return 3; } String dateStr = year + addZero(month) + addZero(day); if(!isCorrectDate(dateStr, dateFormat)){ return 4; } if(isFutureDate(dateStr, dateFormat)){ return 5; } return 0; } /** * DateTimeFormatterを利用して日付チェックを行う * @param dateStr チェック対象文字列 * @param dateFormat 日付フォーマット * @return 日付チェック結果 */ private static boolean isCorrectDate(String dateStr, String dateFormat){ if(isEmpty(dateStr) || isEmpty(dateFormat)){ return false; } //暦体系は和暦体系で、日付と時刻を厳密に解決するスタイルで、 //DateTimeFormatterオブジェクトを作成 DateTimeFormatter df = DateTimeFormatter.ofPattern(dateFormat) .withChronology(JapaneseChronology.INSTANCE) .withResolverStyle(ResolverStyle.STRICT); try{ //チェック対象文字列をLocalDate型の日付に変換できれば、チェックOKとする LocalDate.parse(dateStr, df); return true; }catch(Exception e){ return false; } } /** * 日付の文字列が未来日かどうかを判定する * @param dateStr チェック対象文字列 * @param dateFormat 日付フォーマット * @return 判定結果 */ private static boolean isFutureDate(String dateStr, String dateFormat){ if(!isCorrectDate(dateStr, dateFormat)){ return false; } LocalDate dateStrDate = convertStrToLocalDate(dateStr, dateFormat); LocalDate now = LocalDate.now(); if(dateStrDate.isAfter(now)){ return true; } return false; } /** * 日付の文字列を日付型に変換した結果を返す * @param dateStr 日付の文字列 * @param dateFormat 日付のフォーマット * @return 変換後の文字列 */ private static LocalDate convertStrToLocalDate(String dateStr, String dateFormat){ if(isEmpty(dateStr) || isEmpty(dateFormat)){ return null; } //暦体系は和暦体系で、日付と時刻を厳密に解決するスタイルで、 //DateTimeFormatterオブジェクトを作成 DateTimeFormatter df = DateTimeFormatter.ofPattern(dateFormat, Locale.JAPAN) .withChronology(JapaneseChronology.INSTANCE) .withResolverStyle(ResolverStyle.STRICT); //日付の文字列をLocalDate型に変換して返却 return LocalDate.parse(dateStr, df); } /** * 数値文字列が1桁の場合、頭に0を付けて返す * @param intNum 数値文字列 * @return 変換後数値文字列 */ private static String addZero(String intNum){ if(isEmpty(intNum)){ return intNum; } if(intNum.length() == 1){ return "0" + intNum; } return intNum; } /** * 引数の文字列がnull、空文字かどうかを判定する * @param str チェック対象文字列 * @return 文字列チェック結果 */ private static boolean isEmpty(String str){ if(str == null || "".equals(str)){ return true; } return false; } }
また、入力チェックを行いエラーがあった場合にエラーメッセージを表示し、赤反転するエラ-フィールドは、下記「input.html」にて実施している。それぞれのフィールドに関するエラーメッセージの表示はspanタグにて実施している。さらに、エラーフィールドを赤反転する処理は、それぞれのth:fieldタグとth:errorclassタグにて実施している。
<!DOCTYPE html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <link th:href="@{/demo.css}" rel="stylesheet" type="text/css" /> <title>入力画面</title> </head> <body> <p>下記必要事項を記載の上、「確認」ボタンを押下してください。</p><br/> <form method="post" th:action="@{/confirm}" th:object="${demoForm}"> 名前: <input type="text" th:value="*{name}" th:field="*{name}" th:errorclass="fieldError" /> <span th:if="*{#fields.hasErrors('name')}" th:errors="*{name}" class="errorMessage"></span> <br/> 生年月日: <input type="text" th:value="*{birthYear}" size="4" maxlength="4" th:field="*{birthYear}" th:errorclass="fieldError" />年 <select th:field="*{birthMonth}" th:errorclass="fieldError"> <option value="">---</option> <option th:each="item : *{getMonthItems()}" th:value="${item.key}" th:text="${item.value}"/> </select>月 <select th:field="*{birthDay}" th:errorclass="fieldError"> <option value="">---</option> <option th:each="item : *{getDayItems()}" th:value="${item.key}" th:text="${item.value}"/> </select>日 <span th:if="*{#fields.hasErrors('birthYear')}" th:errors="*{birthYear}" class="errorMessage"></span> <span th:if="*{#fields.hasErrors('birthMonth')}" th:errors="*{birthMonth}" class="errorMessage"></span> <span th:if="*{#fields.hasErrors('birthDay')}" th:errors="*{birthDay}" class="errorMessage"></span> <br/> 性別: <for th:each="item : *{getSexItems()}"> <input type="radio" name="sex" th:value="${item.key}" th:text="${item.value}" th:field="*{sex}" th:errorclass="fieldError" /> </for> <span th:if="*{#fields.hasErrors('sex')}" th:errors="*{sex}" class="errorMessage"></span> <br/> 入力確認: <input type="checkbox" name="checked" th:value="確認済" th:field="*{checked}" th:errorclass="fieldError" /> <span th:if="*{#fields.hasErrors('checked')}" th:errors="*{checked}" class="errorMessage"></span> <br/><br/> <input type="submit" value="確認" /> </form> </body> </html>
また、エラーメッセージ・エラーフィールドの色は、下記「demo.css」にて定義している。
.errorMessage{ color: #FF0000; } .fieldError{ background-color: #FFCCFF; }
さらに、「confirm.html」の内容は以下の通りで、ここでは「送信」「戻る」の各ボタンにnameタグが定義されていて、「送信」ボタンが押下された場合は「DemoController.java」の「params = “next”」が付与されているメソッド(sendメソッド)が、「戻る」ボタンが押下された場合は「DemoController.java」の「params = “back”」が付与されているメソッド(backメソッド)が呼ばれるようになっている。
<!DOCTYPE html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>確認画面</title> </head> <body> <p>入力内容を確認し、問題なければ「送信」ボタンを押下してください。</p> <form method="post" th:action="@{/send}" th:object="${demoForm}"> <p th:text="'名前: ' + *{name}">ここに名前が表示されます</p> <p th:text="'生年月日: ' + *{birthYear} + '年' + *{getMonthItems().get('__*{birthMonth}__')} + '月' + *{getDayItems().get('__*{birthDay}__')} + '日'"> ここに生年月日が表示されます </p> <p th:text="'性別: ' + *{getSexItems().get('__*{sex}__')}"> ここに性別が表示されます </p> <p th:text="'確認チェック: ' + *{checked}"> ここに確認チェック内容が表示されます </p> <input type="submit" name="next" value="送信" /> <input type="submit" name="back" value="戻る" /> </form> </body> </html>
その他、「complete.html」「application.properties」の内容は以下の通り。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>完了画面</title> </head> <body> お申し込みが完了しました。<br/><br/> <input type="button" value="閉じる" onclick="window.close();" /> </body> </html>
server.port = 8084
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-web-check2/demo
また、その他のチェック用アノテーションを利用した場合の実装方法は、下記サイトが参考になる。
https://www.tuyano.com/index3?id=6209570301018112&page=5
要点まとめ
- チェック処理は、formオブジェクトのアノテーションや、コントローラクラス上での独自実装によって行える。また、formオブジェクトのアノテーションは、「@Valid」アノテーションで有効化される。
- エラーメッセージは、th:errorsタグで表示する。また、HTMLファイル上で各フォームの色を変更するには、th:fieldタグとth:errorclassタグを利用する。
- エラーメッセージは、SpringBoot起動用クラスに所定の処理を記述することで、「application.properties」に変更できる。
- 同一アクション内でsubmit処理を複数に分けるには、HTMLファイル上でnameタグを、コントローラクラス上でparams属性を利用する。