かんがるーさんの日記

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

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その31 )( 貸出申請画面の作成2 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その30 )( 貸出申請画面の作成 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 貸出申請画面の作成
      • パラメータで指定されている貸出申請 ID の書籍と貸出状況の一覧を DB から取得する処理の実装
    • 今回は共通エラー画面も作成します。

参照したサイト・書籍

  1. PostgreSQL 9.4.4文書 - 第 8章データ型 - 8.1. 数値データ型
    https://www.postgresql.jp/document/9.4/html/datatype-numeric.html

    • PostgreSQL の bigint の最大値を確認する時に参照しました。
  2. Hibernate Validator 5.2.2.Final - Chapter 2. Declaring and validating bean constraints
    http://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html/ch02.html

    • Hibernate Validator で数値チェックするアノテーションを調べる時に参照しました。
    • 5.2.2.Final のマニュアルを見ていますが、Spring Boot 1.2.7 で使用されるバージョンは 5.1.3.Final です。最新のマニュアルのみ参照可能なようでしたので 5.2.2.Final のマニュアルを参照しました。
  3. Java の Short, Integer, Long, Float, Double 型の最大値 / 最小値 & それぞれの値を漢数字表記すると
    http://samuraism.jp/diary/2008/10/06/1223272200000.html

    • Doma 2 で生成した LendingApp の Entity クラスで PostgreSQL の bigint 型が Java の Long 型にマッピングされていたので、Long 型の最大値を確認した時に参照しました。
  4. Exception Handling in Spring MVC
    https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc

    • 共通エラー画面を実装する際に参照しました。
  5. thymeleaf/thymeleaf-extras-java8time
    https://github.com/thymeleaf/thymeleaf-extras-java8time

    • Thymeleaf テンプレートファイルで Java 8 の Date and Time API で取得した日時データをフォーマットして出力する方法を調べている時に参照しました。
  6. HOW-TO: JAVA 8 DATE & TIME WITH THYMELEAF AND SPRING BOOT
    http://blog.codeleak.pl/2015/11/how-to-java-8-date-time-with-thymeleaf.html

    • Thymeleaf テンプレートファイルで Java 8 の Date and Time API で取得した日時データをフォーマットして出力する方法を調べている時に参照しました。
  7. package 配下のクラス一覧を取得する方法いろいろ
    http://etc9.hatenablog.com/entry/2015/03/31/001620

    • パッケージ内のクラス一覧を取得する方法を調べた時に参照しました。
  8. How can I convert a stack trace to a string?
    http://stackoverflow.com/questions/1149703/how-can-i-convert-a-stack-trace-to-a-string

  9. リフレクション
    http://www.ne.jp/asahi/hishidama/home/tech/java/reflection.html#getEnumConstants()

    • Java のリフレクションの使い方を調べた時に参照しました。
  10. Invoking a static method using reflection
    http://stackoverflow.com/questions/2467544/invoking-a-static-method-using-reflection

    • リフレクションで static メソッドを呼び出す方法を調べた時に参照しました。
  11. Tutorial: Using Thymeleaf - 6.2 Keeping iteration status
    http://www.thymeleaf.org/doc/tutorials/2.1/usingthymeleaf.html#keeping-iteration-status

  12. Tutorial: Thymeleaf + Spring - 7.6 Dynamic fields
    http://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#dynamic-fields

    • 貸出申請画面の一覧上に Form クラスのデータを表示させる方法を調べた時に参照しました。

目次

  1. LendingappParamForm, LendingappForm クラスの作成
  2. messages_ja_JP.properties にエラーメッセージを定義する
  3. LendingappService クラスの作成
  4. 共通エラー画面の作成
  5. LendingappController クラスの変更
  6. LendingBookLendingAppFlgValues クラスの作成
  7. ValuesHelper クラスの作成
  8. lendingapp.html の変更
  9. 動作確認
  10. 次回は。。。

手順

LendingappParamForm, LendingappForm クラスの作成

  1. 最初に画面を表示する時と「申請」「一時保存」ボタンがクリックされた時で Bean Validation を実行する対象を分けたいため、Form クラスを2つ作成します。

  2. 最初に Request 時に渡される URL パラメータだけを受け取る LendingappParamForm クラスを作成します。こちらは最初に画面を表示する時に Bean Validation を実行します。src/main/java/ksbysample/webapp/lending/web/lendingapp の下に LendingappParamForm.java を作成します。作成後、リンク先の内容 に変更します。

  3. 次に画面の一覧上に表示するデータを格納する Form クラスを作成します。こちらは最初に画面を表示する時に Bean Validation を実行しません。src/main/java/ksbysample/webapp/lending/web/lendingapp の下に LendingappForm.java を作成します。作成後、リンク先の内容 に変更します。

messages_ja_JP.properties にエラーメッセージを定義する

  1. エラーメッセージを定義します。src/main/resources の下の messages_ja_JP.properties を リンク先の内容 に変更します。

LendingappService クラスの作成

  1. src/main/java/ksbysample/webapp/lending/web/lendingapp の下に LendingappService.java を作成します。作成後、リンク先の内容 に変更します。

共通エラー画面の作成

  1. LendingappService クラスで WebApplicationRuntimeException が throw された時にエラーメッセージを表示するために共通エラー画面を実装します。以下の方法で実装します。

    • @ControllerAdvice アノテーションを付加した ExceptionHandlerAdvice クラスを作成し、その中で @ExceptionHandler(Exception.class) アノテーション を付加した handleException メソッドを実装します。
    • ExceptionHandlerAdvice クラスだけだと想定していない例外が発生した時にこれまで表示されていた白い背景のエラー画面が表示されなくなりますので、ErrorController インターフェースを実装した WebappErrorController クラスを作成します。WebappErrorController クラスの getErrorPath メソッド/error を返すようにします。
    • 最後に src/main/resources/templates の下に error.html を作成し、ExceptionHandlerAdvice#handleException メソッド内で取得した情報が表示されるようにします。
  2. 共通エラー画面で Java 8 の Date and Time API で取得した日時をフォーマットして出力できるようにします。build.gradle を リンク先の内容 に変更します。

  3. Gradle projects View の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

  4. src/main/java/ksbysample/webapp/lending/config の下の ApplicationConfig.javaリンク先の内容 に変更します。

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

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

  7. src/main/resources/templates の下に error.html を作成します。作成後、リンク先の内容 に変更します。

LendingappController クラスの変更

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

LendingBookLendingAppFlgValues クラスの作成

  1. 「申請」欄の「する」「しない」のドロップダウンリストを表示するために使用する LendingBookLendingAppFlgValues クラスを作成します。

  2. src/main/java/ksbysample/webapp/lending/values の下に Values.java を作成します。作成後、リンク先の内容 に変更します。このインターフェースは次に説明する ValuesHelper クラスで利用します。

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

ValuesHelper クラスの作成

  1. 現在の実装では Values クラスを Thymeleaf テンプレートファイルから呼び出す時に ${T(ksbysample.webapp.lending.values.LendingBookLendingAppFlgValues).getText(...)} のようにパッケージ名まで記述する必要があるため、記述をもう少し簡略にするための ValuesHelper クラスを作成します。

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

lendingapp.html の変更

  1. src/main/resources/templates/lendingapp の下の lendingapp.html を リンク先の内容 に変更します。

動作確認

  1. 事前に lending_app, lending_book テーブルのデータを全て削除します。

  2. メールを受信するので smtp4dev が起動していない場合には起動します。

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

  4. ログインして貸出希望書籍 CSV ファイルアップロード画面からテスト.csv をアップロードし、貸出状況取得タスクからのメールを受信します。

    f:id:ksby:20151120064242p:plain

    f:id:ksby:20151120064416p:plain

    f:id:ksby:20151120064525p:plain

    f:id:ksby:20151120064637p:plain

  5. メールの URL をクリックして貸出申請画面を表示します。画面が表示されることが確認できます。

    f:id:ksby:20151120064903p:plain

  6. DB のデータを変更して、変更したデータが表示されるか確認します。最初に以下の画像のデータに更新します ( 赤枠の部分を変更しています。 )。

    f:id:ksby:20151120065409p:plain

  7. ブラウザで F5 キーを押してリロードします。変更したデータで画面上に表示されることが確認できます。

    f:id:ksby:20151120065629p:plain

  8. 共通エラー画面の動作確認を行います。最初に存在しない貸出申請IDで貸出申請画面にアクセスしてみます。ブラウザで http://localhost:8080/lendingapp?lendingAppId=1 にアクセスします。

    共通エラー画面が表示され、「指定された貸出申請IDのデータが登録されておりません。」のエラーメッセージが画面上に表示されていることが確認できます。

    f:id:ksby:20151120070114p:plain

  9. 次に存在しない URL にアクセスしてみます。ブラウザで http://localhost:8080/lendingxxx にアクセスします。

    共通エラー画面が表示され、エラーメッセージには「404 Not Found」が表示されることが確認できます。ただし、URL が http://localhost:8080/error と表示されていますね。。。 今回はこのまま進めますが、元の URL を表示することができるのか後で調査したいと思います。

    f:id:ksby:20151120070827p:plain

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

  11. 一旦 commit します。

次回は。。。

「申請」ボタンクリック時の処理を実装する予定です。

ソースコード

LendingappParamForm.java

package ksbysample.webapp.lending.web.lendingapp;

import lombok.Data;

import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;

@Data
public class LendingappParamForm {

    @DecimalMin(value = "1")
    @DecimalMax(value = "9223372036854775807")
    private Long lendingAppId;

}

LendingappForm.java

package ksbysample.webapp.lending.web.lendingapp;

import ksbysample.webapp.lending.entity.LendingApp;
import ksbysample.webapp.lending.entity.LendingBook;
import lombok.Data;

import java.util.List;

@Data
public class LendingappForm {

    private LendingApp lendingApp;

    private List<LendingBook> lendingBookList;
    
}

messages_ja_JP.properties

.....
UploadBooklistForm.fileupload.bookname.lengtherr={0}行目の書名のデータの文字数が128文字以内でありません ( {1} )。

LendingappForm.lendingAppId.emptyerr=貸出申請IDが指定されていません。
LendingappForm.lendingApp.nodataerr=指定された貸出申請IDのデータが登録されておりません。
  • LendingappForm.lendingAppId.emptyerr, LendingappForm.lendingApp.nodataerr を追加します。

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.message.MessagesPropertiesHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class LendingappService {

    @Autowired
    private LendingAppDao lendingAppDao;

    @Autowired
    private LendingBookDao lendingBookDao;

    @Autowired
    private MessagesPropertiesHelper messagesPropertiesHelper;

    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;
    }

}

build.gradle

dependencies {
    def jdbcDriver = "org.postgresql:postgresql:9.4-1204-jdbc41"

    // spring-boot-gradle-plugin によりバージョン番号が自動で設定されるもの
    // Appendix E. Dependency versions ( http://docs.spring.io/spring-boot/docs/current/reference/html/appendix-dependency-versions.html ) 参照
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity3")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.boot:spring-boot-starter-velocity")
    compile("org.springframework.boot:spring-boot-starter-mail")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.springframework.boot:spring-boot-starter-redis")
    compile("org.springframework.boot:spring-boot-starter-amqp")
    compile("org.codehaus.janino:janino")
    compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
    compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
    testCompile("org.springframework.boot:spring-boot-starter-test")
    // (ここから) gradle でテストを実行した場合に spring-security-test-4.0.2.RELEASE.jar しか classpath に指定されず
    // テストが失敗したため、3.2.8.RELEASE を明記している
    testCompile("org.springframework.security:spring-security-core:3.2.8.RELEASE")
    testCompile("org.springframework.security:spring-security-web:3.2.8.RELEASE")
    // (ここまで) ------------------------------------------------------------------------------------------------------
    testCompile("org.springframework.security:spring-security-test:4.0.2.RELEASE")
    testCompile("org.yaml:snakeyaml")

    // spring-boot-gradle-plugin によりバージョン番号が自動で設定されないもの
    compile("${jdbcDriver}")
    compile("org.seasar.doma:doma:2.5.0")
    compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")
    compile("org.apache.commons:commons-lang3:3.4")
    compile("org.projectlombok:lombok:1.16.6")
    compile("com.google.guava:guava:18.0")
    compile("org.springframework.session:spring-session:1.0.1.RELEASE")
    compile("org.simpleframework:simple-xml:2.7.1")
    compile("com.univocity:univocity-parsers:1.5.6")
    compile("org.thymeleaf.extras:thymeleaf-extras-java8time:2.1.0.RELEASE")
    testCompile("org.dbunit:dbunit:2.5.1")
    testCompile("com.icegreen:greenmail:1.4.1")
    testCompile("org.assertj:assertj-core:3.2.0")
    testCompile("com.jayway.jsonpath:json-path:2.0.0")
    testCompile("org.jmockit:jmockit:1.19")

    // for Doma-Gen
    domaGenRuntime("org.seasar.doma:doma-gen:2.5.0")
    domaGenRuntime("${jdbcDriver}")
}
  • compile("org.thymeleaf.extras:thymeleaf-extras-java8time:2.1.0.RELEASE") を追加します。

ApplicationConfig.java

package ksbysample.webapp.lending.config;

import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;

@Configuration
public class ApplicationConfig {

    @Autowired
    private ConnectionFactory connectionFactory;
    
    @Bean
    public Queue inquiringStatusOfBookQueue() {
        return new Queue(Constant.QUEUE_NAME_INQUIRING_STATUSOFBOOK, false);
    }

    @Bean
    public MessageConverter messageConverter() {
        Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
        return converter;
    }

    @Bean
    public RabbitTemplate rabbitTemplate() {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(this.connectionFactory);
        rabbitTemplate.setMessageConverter(this.messageConverter());
        return rabbitTemplate;
    }

    @Bean
    public Java8TimeDialect java8TimeDialect() {
        return new Java8TimeDialect();
    }

}
  • Java8TimeDialect Bean を追加します。

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.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.ArrayList;
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) {
        String url = request.getRequestURL().toString()
                + (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());
        } 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);
        // スタックトレース
        if (e != null) {
            errorInfoList.add(" ");
            for (StackTraceElement trace : e.getStackTrace()) {
                errorInfoList.add(trace.toString());
            }
        }
        
        return model;
    }

}

WebappErrorController.java

package ksbysample.webapp.lending.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Controller
@RequestMapping("/error")
public class WebappErrorController implements ErrorController {

    @Autowired
    private ExceptionHandlerAdvice exceptionHandlerAdvice;

    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping
    public ModelAndView index(Exception e, HttpServletRequest request, HttpServletResponse response) {
        ModelAndView model = exceptionHandlerAdvice.handleException(e, request, response);
        return model;
    }
    
}

error.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <title>共通エラー画面</title>
    <!-- Tell the browser to be responsive to screen width -->
    <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/>
    <link th:replace="common/head-cssjs"/>

    <style>
        .content-wrapper {
            background-color: #fffafa;
        }
    </style>
</head>

<!-- ADD THE CLASS layout-top-nav TO REMOVE THE SIDEBAR. -->
<body class="skin-blue layout-top-nav">
<div class="wrapper">

    <!-- Main Header -->
    <header class="main-header" th:fragment="main-header">
        <nav class="navbar navbar-static-top">
            <div class="container">
                <div class="navbar-header">
                    <a href="#" class="navbar-brand"><b>ksbysample-lending</b></a>
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse">
                        <i class="fa fa-bars"></i>
                    </button>
                </div>

                <!-- Collect the nav links, forms, and other content for toggling -->
                <div class="collapse navbar-collapse pull-left" id="navbar-collapse">
                    <ul class="nav navbar-nav">
                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown">メニュー <span class="caret"></span></a>
                            <ul class="dropdown-menu" role="menu">
                                <li><a href="/booklist">貸出希望書籍登録</a></li>
                                <li><a href="/lendingapp">貸出申請</a></li>
                                <li><a href="/confirmresult">貸出申請結果確認</a></li>
                                <li class="divider"></li>
                                <li><a href="/lendingapproval">貸出承認</a></li>
                                <li class="divider"></li>
                                <li><a href="/admin/library">検索対象図書館登録</a></li>
                            </ul>
                        </li>
                    </ul>
                </div>
                <!-- /.navbar-collapse -->

                <!-- Navbar Right Menu -->
                <div class="navbar-custom-menu">
                    <ul class="nav navbar-nav">
                        <li><a href="/logout">ログアウト</a></li>
                    </ul>
                </div>
                <!-- /.navbar-custom-menu -->
            </div>
            <!-- /.container-fluid -->
        </nav>
    </header>

    <!-- Full Width Column -->
    <div class="content-wrapper">
        <div class="container">
            <!-- Main content -->
            <section class="content">
                <div class="row">
                    <div class="col-xs-12">
                        <div class="callout callout-danger">
                            <h4><i class="fa fa-warning"></i> エラーが発生しました。</h4>
                            <div th:if="${errorMessage}">
                                <span th:text="${errorMessage}">エラーメッセージ</span><br/>
                            </div>
                            発生日時: <span th:text="${#temporals.format(currentdt, 'yyyy/MMM/dd HH:mm:ss')}">9999/99/99 99:99</span><br/>
                            <div th:each="errorInfo : ${errorInfoList}">
                                <span th:text="${errorInfo}">エラー情報</span>
                            </div>
                        </div>
                    </div>
                </div>
            </section>
            <!-- /.content -->
        </div>
        <!-- /.container -->
    </div>

</div>
<!-- ./wrapper -->

<script th:replace="common/bottom-js"></script>
</body>
</html>

LendingappController.java

package ksbysample.webapp.lending.web.lendingapp;

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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.List;

@Controller
@RequestMapping("/lendingapp")
public class LendingappController {

    @Autowired
    private LendingappService lendingappService;

    @Autowired
    private MessagesPropertiesHelper messagesPropertiesHelper;
    
    @RequestMapping
    public String index(@Validated LendingappParamForm lendingappParamForm
            , BindingResult bindingResultForLendingappParamForm
            , LendingappForm lendingappForm) {
        if (bindingResultForLendingappParamForm.hasErrors()) {
            throw new WebApplicationRuntimeException(
                    messagesPropertiesHelper.getMessage("LendingappForm.lendingAppId.emptyerr", null));
        }

        LendingApp lendingApp = lendingappService.getLendingApp(lendingappParamForm.getLendingAppId());
        List<LendingBook> lendingBookList = lendingappService.getLendingBookList(lendingappParamForm.getLendingAppId());
        lendingappForm.setLendingApp(lendingApp);
        lendingappForm.setLendingBookList(lendingBookList);
        
        return "lendingapp/lendingapp";
    }

    @RequestMapping(value = "/apply", method = RequestMethod.POST)
    public String apply() {
        return "lendingapp/lendingapp";
    }

    @RequestMapping(value = "/temporarySave", method = RequestMethod.POST)
    public String temporarySave() {
        return "lendingapp/lendingapp";
    }

}
  • index メソッドの以下の点を変更します。
    • lendingappParamForm, bindingResultForLendingappParamForm, lendingappForm の3つの引数を追加します。
    • 内部の処理を実装します。

Values.java

package ksbysample.webapp.lending.values;

public interface Values {

    public String getValue();

    public String getText();

}

LendingBookLendingAppFlgValues.java

package ksbysample.webapp.lending.values;

import lombok.Getter;
import org.apache.commons.lang3.StringUtils;

@Getter
public enum LendingBookLendingAppFlgValues implements Values {

    NOT_APPLY("", "しない")
    , APPLY("1", "する");

    private final String value;
    private final String text;

    LendingBookLendingAppFlgValues(String value, String text) {
        this.value = value;
        this.text = text;
    }

    public static String getText(String value) {
        String result = "";
        for (Values val : LendingBookLendingAppFlgValues.values()) {
            if (StringUtils.equals(val.getValue(), value)) {
                result = val.getText();
            }
        }

        return result;
    }

}

ValuesHelper.java

package ksbysample.webapp.lending.values;

import com.google.common.reflect.ClassPath;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.stream.Collectors;

@Component("vh")
public class ValuesHelper {

    private final Map<String, String> valuesObjList;
    
    private ValuesHelper() throws IOException {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        valuesObjList = ClassPath.from(loader).getTopLevelClasses(this.getClass().getPackage().getName())
                .stream()
                .filter(classInfo -> !StringUtils.equals(classInfo.getName(), this.getClass().getName()))
                .collect(Collectors.toMap(ClassPath.ClassInfo::getSimpleName, ClassPath.ClassInfo::getName));
    }
    
    public String getText(String classSimpleName, String value)
            throws ClassNotFoundException, IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class<?> clazz = Class.forName(this.valuesObjList.get(classSimpleName));
        Method method = clazz.getMethod("getText", String.class);
        String result = (String) method.invoke(null, value);
        return result;
    }

    public Values[] values(String classSimpleName)
            throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class<?> clazz = Class.forName(this.valuesObjList.get(classSimpleName));
        Method method = clazz.getMethod("values");
        Values[] result = (Values[]) method.invoke(null);
        return result;
    }
    
}
  • DI コンテナでのインスタンス生成時にこのクラスと同じパッケージ内にある Values クラス一覧をフィールド valuesObjList に保存します。クラス一覧の取得には Guava の ClassPath を利用しています。
  • Thymeleaf テンプレートファイルからは ${@vh.getText('Values クラス名', '値(value)')}${@vh.values('Values クラス名')} の形式で呼び出し、リフレクションで Values クラスの getText, values メソッドを呼び出します。

lendingapp.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <title>貸出申請</title>
    <!-- Tell the browser to be responsive to screen width -->
    <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/>
    <link th:replace="common/head-cssjs"/>

    <style>
        .content-wrapper {
            background-color: #fffafa;
        }
        .selected-library {
            color: #ffffff !important;
            font-size: 100%;
            font-weight: 700;
        }
        .box-body.no-padding {
            padding-bottom: 10px !important;
        }
        .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;
            font-size: 90%;
        }
        .jp-gothic {
            font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
        }
    </style>
</head>

<!-- ADD THE CLASS layout-top-nav TO REMOVE THE SIDEBAR. -->
<body class="skin-blue layout-top-nav">
<div class="wrapper">

    <!-- Main Header -->
    <div th:replace="common/mainparts :: main-header"></div>

    <!-- Full Width Column -->
    <div class="content-wrapper">
        <div class="container">
            <!-- Content Header (Page header) -->
            <section class="content-header">
                <h1>貸出申請</h1>
            </section>

            <!-- Main content -->
            <section class="content">
                <div class="row">
                    <div class="col-xs-12">
                        <form id="lendingappForm" method="post" action="/lendingapp/apply" th:action="@{/lendingapp/apply}" th:object="${lendingappForm}">
                            <div class="box">
                                <div class="box-body no-padding">
                                    <div class="col-xs-6 no-padding">
                                        <table class="table table-bordered">
                                            <colgroup>
                                                <col width="30%"/>
                                                <col width="70%"/>
                                            </colgroup>
                                            <tr>
                                                <th class="bg-purple">貸出申請ID</th>
                                                <th th:text="*{lendingApp.lendingAppId}">1</th>
                                            </tr>
                                            <tr>
                                                <th class="bg-purple">ステータス</th>
                                                <th th:text="${@vh.getText('LendingAppStatusValues', lendingappForm.lendingApp.status)}">申請中</th>
                                            </tr>
                                        </table>
                                    </div>
                                    <br/>
    
                                    <table class="table table-hover">
                                        <colgroup>
                                            <col width="5%"/>
                                            <col width="20%"/>
                                            <col width="20%"/>
                                            <col width="15%"/>
                                            <col width="15%"/>
                                            <col width="25%"/>
                                        </colgroup>
                                        <thead class="bg-purple">
                                        <tr>
                                            <th>No.</th>
                                            <th>ISBN</th>
                                            <th>書名</th>
                                            <th>貸出状況</th>
                                            <th>申請</th>
                                            <th>申請理由</th>
                                        </tr>
                                        </thead>
                                        <tbody class="jp-gothic">
                                        <tr th:each="lendingBook, iterStat : *{lendingBookList}">
                                            <th th:text="${iterStat.count}">1</th>
                                            <th th:text="${lendingBook.isbn}">978-1-4302-5908-4</th>
                                            <th th:text="${lendingBook.bookName}">Spring Recipes</th>
                                            <th th:text="${lendingBook.lendingState}">蔵書なし</th>
                                            <th>
                                                <select class="form-control input-sm"
                                                        th:field="*{lendingBookList[__${iterStat.index}__].lendingAppFlg}"
                                                        th:if="${lendingBook.lendingState} == '蔵書あり'">
                                                    <option th:each="lendingAppFlg : ${@vh.values('LendingBookLendingAppFlgValues')}"
                                                            th:value="${lendingAppFlg.getValue()}"
                                                            th:text="${lendingAppFlg.getText()}">しない</option>
                                                </select>
                                            </th>
                                            <th>
                                                <input type="text" class="form-control input-sm"
                                                       th:field="*{lendingBookList[__${iterStat.index}__].lendingAppReason}"
                                                       th:if="${lendingBook.lendingState} == '蔵書あり'"/>
                                            </th>
                                            <input type="hidden" th:field="*{lendingBookList[__${iterStat.index}__].lendingBookId}"/>
                                        </tr>
                                        </tbody>
                                    </table>
                                    <div class="text-center">
                                        <button class="btn bg-blue js-btn-apply"><i class="fa fa-thumbs-up"></i> 申請</button>
                                        <button class="btn bg-orange js-btn-temporarySave"><i class="fa fa-tag"></i> 一時保存(まだ申請しない)</button>
                                    </div>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </section>
            <!-- /.content -->
        </div>
        <!-- /.container -->
    </div>

</div>
<!-- ./wrapper -->

<script th:replace="common/bottom-js"></script>
<script type="text/javascript">
    <!--
    $(document).ready(function() {
        $(".js-btn-apply").click(function(){
            $("#lendingappForm").submit();
            return false;
        });

        $(".js-btn-temporarySave").click(function(){
            $("#lendingappForm").attr("action", "/lendingapp/temporarySave");
            $("#lendingappForm").submit();
            return false;
        });
    });
    -->
</script>
</body>
</html>
  • 貸出申請ID、ステータスに Form オブジェクトの値を表示させるために <form id="lendingappForm" method="post" action="/lendingapp/apply" th:action="@{/lendingapp/apply}" th:object="${lendingappForm}"><div class="box"> の上に移動します。</form> も対応する位置へ移動します。
  • ステータスは ValuesHelper クラスを利用して th:text="${@vh.getText('LendingAppStatusValues', lendingappForm.lendingApp.status)}" でコードに対応する文字列が表示されるようにします。
  • 一覧表では <tr th:each="lendingBook, iterStat : *{lendingBookList}"> のようにイテレーション処理中のステータスを取るための変数 iterStat も宣言します。
  • 申請する/しないのドロップダウンリストでは、<option th:each="lendingAppFlg : ${@vh.values('LendingBookLendingAppFlgValues')}" ... </option> の記述により「する」「しない」のリストが表示されます。また select タグに th:field="*{lendingBookList[__${iterStat.index}__].lendingAppFlg}" を記述することで値が等しい項目に selected="selected" 属性が自動的に付加されます。
  • 申請理由の入力フィールドも th:field="*{lendingBookList[__${iterStat.index}__].lendingAppReason}" を記述して Form オブジェクトと関連付けます。
  • 申請する/しないのドロップダウンリスト、申請理由の入力フィールドのどちらも name 属性を削除します。Thymeleaf の機能により自動生成された id 属性、name 属性が出力されます。
  • 更新時にキー項目が必要になるので、<input type="hidden" th:field="*{lendingBookList[__${iterStat.index}__].lendingBookId}"/> を追加します。

履歴

2015/11/20
初版発行。