かんがるーさんの日記

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

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

概要

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

  • 今回の手順で確認できるのは以下の内容です。
    • 共通エラー画面で 404 Not Found の時に /error ではなく元の URL を表示させる&スタックトレースの出力フォーマットを変更する
    • 貸出申請画面の作成
      • 「申請」ボタンクリック時の処理の実装

参照したサイト・書籍

  1. Getting an URL which led to error (404) from error-page controller in spring MVC
    http://stackoverflow.com/questions/13900650/getting-an-url-which-led-to-error-404-from-error-page-controller-in-spring-mvc

    • 作成した共通エラー画面で 404 Not Found の時に「エラーが発生する URL」に http://localhost:8080/error ではなく元の URL を表示させる方法を調査した時に参照しました。
  2. Javaの例外のスタックトレースをStringに変換するスニペット
    http://qiita.com/sifue/items/07388fdada096734fa7f

目次

  1. 共通エラー画面で 404 Not Found の時に /error ではなく元の URL を表示させる
  2. 共通エラー画面のスタックトレースの表示フォーマットを修正する
  3. LendingBookDto クラスの作成、LendingappForm クラスの変更
  4. LendingappFormValidator クラスの作成
  5. LendingBookDao, LendingappService クラスの変更
  6. LendingappController クラスの変更
  7. ValuesHelper, LendingAppStatusValues クラスの変更
  8. lendingapp.html の変更
  9. 動作確認
  10. 次回は。。。
  11. メモ書き
    1. 一覧表形式の入力画面を作成するポイント

手順

共通エラー画面で 404 Not Found の時に /error ではなく元の URL を表示させる

stackoverflow の Getting an URL which led to error (404) from error-page controller in spring MVC の回答では request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI) で元の URL を取得できるとありました。ただし RequestDispatcher の定数一覧を見てみたところ ERROR_REQUEST_URI というものがあり、実装して Debugger で見てみたところこちらにも元の URI が入っていましたので request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI) で取得するようにしたいと思います。

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

  2. Tomcat を起動してブラウザから存在しない URL である http://localhost:8080/lendingap?lendingAppId=104 にアクセスすると以下の共通エラー画面が表示されます。

    「エラーが発生したURL」にエラー発生原因の URI を表示されていることが確認できます。

    f:id:ksby:20151121062336p:plain

  3. 一旦 commit します。

共通エラー画面のスタックトレースの表示フォーマットを修正する

共通エラー画面に出力しているスタックトレースが少し見にくいなあと思って他の方法があるのか調べていたのですが、StringWriter&PrintWriter を使用して出力する方が通常ログに出力されるのと同じフォーマットで分かりやすかったので修正します。

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

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

  3. Tomcat を起動してブラウザから存在しない URL である http://localhost:8080/lendingap?lendingAppId=104 にアクセスすると以下の共通エラー画面が表示されます。

    f:id:ksby:20151121145606p:plain

LendingBookDto クラスの作成、LendingappForm クラスの変更

一覧上の行毎に Bean Validation で入力チェックをしようと思いましたが、Entity クラスの LendingBook クラスをそのまま使用しているため設定できないことに気づきました。。。 Entiry クラスのデータをコピーする Dto クラスを作成して、画面上に表示するデータは Dto クラスのものを使用するように変更します。

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

  2. src/main/java/ksbysample/webapp/lending/web/lendingapp の下の LendingappForm クラスを リンク先の内容 に変更します。

LendingappFormValidator クラスの作成

Validator クラスを作成して以下の入力チェックを行います。

  • 最低1つ「申請する」が選択されているか。
  • 「申請する」が選択されている場合に申請理由が入力されているか。

 
1. LendingappFormValidator クラスの入力チェックで使用するエラーメッセージを定義します。src/main/resources の下の messages_ja_JP.properties を リンク先の内容 に変更します。

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

LendingBookDao, LendingappService クラスの変更

  1. 更新処理を実装します。最初に lending_book テーブルの lendingAppFlg, lendingAppReason だけを更新するメソッドを作成します。src/main/java/ksbysample/webapp/lending/dao の下の LendingBookDao.javaリンク先の内容 に変更します。

  2. 次に更新処理本体を実装します。src/main/java/ksbysample/webapp/lending/web/lendingapp の下の LendingappService.javaリンク先の内容 に変更します。

LendingappController クラスの変更

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

ValuesHelper, LendingAppStatusValues クラスの変更

  1. Thymeleaf テンプレートファイル内で Values クラスの値を文字列で指定して取得したいので、ValuesHelper クラスに getValue メソッドを追加します。src/main/java/ksbysample/webapp/lending/web/values の下の ValuesHelper.javaリンク先の内容 に変更します。

  2. LendingAppStatusValues クラスに対して ValuesHelper#getValue メソッドを使用したいので、src/main/java/ksbysample/webapp/lending/web/values の下の LendingAppStatusValues.javaリンク先の内容 に変更します。

lendingapp.html の変更

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

動作確認

動作確認します。データは Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その31 )( 貸出申請画面の作成2 ) で作成した貸出申請ID = 105 のデータを使用します。

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

  2. メールの URL をクリックして貸出申請画面を表示します。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。

    f:id:ksby:20151123145943p:plain

    f:id:ksby:20151123150309p:plain

  3. 何も変更せずに「申請」ボタンをクリックした場合にはエラーになることを確認します。「申請」ボタンをクリックします。

    画面上にエラーメッセージが表示され、「申請する/しない」欄の背景色が全て薄い赤色になることが確認できます。

    f:id:ksby:20151123150640p:plain

  4. 「申請する」を選択しても申請理由を記入していない場合にはエラーになることを確認します。何件か「申請する」に変更した後、「申請」ボタンをクリックします。

    画面上にエラーメッセージが表示され、「申請する」を選択した行の「申請理由」欄の背景色が薄い赤色になることが確認できます。

    f:id:ksby:20151123151112p:plain

  5. 「申請理由」欄に 129文字以上入力された場合にはエラーになることを確認します。「申請する」を選択した行の1つの「申請理由」欄に 129文字以上の文字列を入力し、それ以外には適当にエラーにならない文字列を入力した後、「申請」ボタンをクリックします。

    129文字以上の文字列を入力した「申請理由」欄の背景色が薄い赤色になることが確認できます。エラーメッセージは出力するよう実装していませんので、何も表示されません。

    f:id:ksby:20151123151540p:plain

  6. 通常動作を確認します。最初に DB のデータの状態を確認します。

    f:id:ksby:20151123152219p:plain

    何件か「申請する」に変更した後、「申請理由」欄に 128文字以下の文字列を入力して「申請」ボタンをクリックします。

    f:id:ksby:20151123152406p:plain

    画面上部の「ステータス」欄の表示が「申請中」に変わり、「申請する/しない」「申請理由」欄がテキスト表示へ、「申請」「一時保存」ボタンが非表示になります。

    f:id:ksby:20151123153505p:plain

    申請後の DB のデータの状態を確認すると lending_app.status の値が 3(申請中) へ更新されており、lending_book のデータのうち画面で「申請する」を選択したデータの lending_app_flg の値が 1(申請する) に更新され、lending_app_reason に画面の「申請理由」欄に入力された文字列がセットされていることが確認できます。

    f:id:ksby:20151123153912p:plain

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

  8. 一旦 commit します。

次回は。。。

最後に表示した貸出申請の ID を Cookie に保存し、Cookie がある場合には次回ログイン時に前回表示していた貸出申請画面を表示する機能を実装する予定です。

メモ書き

一覧表形式の入力画面を作成するポイント

1度実装してコツが分かればそんなに難しくないのですが、最初に分かるまでがちょっと時間がかかるのでポイントをメモしておきます。

  • 最初に概要を書くと、
    • 一覧表の行に対応する子 Form クラスを作ること。親 Form クラスには List で定義する。
    • 子 Form クラスに Bean Validation は定義可能。親 Form クラスで @Valid を付加すること。
    • Thymeleaf テンプレートファイルでは th:each で status variable (例:iterStat) を定義する。
    • th:field や fields.hasErrors でフィールド名を記述する時は status variable を使用する。
    • 行別に Errors#rejectValue でエラーをセットすることも可能。ただしフィールド名に注意。
  • Form クラスを親子構成にし、子の Form クラス ( 今回のように Dto でも可 ) に一覧表の行単位の入力フィールドを定義します。
  • 親の Form クラスに子の Form クラスを記述する時には List<子Formクラス> 型で記述します。
  • 子の Form クラスで Bean Validation を使用したい場合には、親 Form クラス内の子 Form クラスを記述しているフィールドに @Valid アノテーションを付加します。あとは Controller クラスのメソッドで親 Form クラスの引数に @Validated アノテーションを付加すれば子 Form クラスの Bean Validation が実行されます。
  • Thymeleaf テンプレートファイルの記述は以下のようにします。

    f:id:ksby:20151123160629p:plain

    • th:each に status variable を定義します ( 上の図の iterStat )。
    • th:field は th:field="*{ + 子FormクラスのList名 + [__iterStat.index}__]. + 子Formクラスのフィールド + }" ( status variable が iterStat でない場合には適宜変更すること ) の形式で記述します。
    • #fields.hasErrors(...) で各行のフィールドのエラーをチェックしたい場合には th:field と同じ形式でフィールドを指定します。
    • 入力項目がないがデータを子 Form クラスにセットしたい場合には <input type="hidden" th:feid="..."/> で定義します。
    • 入力フィールドの input, select 等のタグには id, name 属性を記述しません。Thymeleaf が自動で生成した文字列が出力されます。以下に出力された HTML の例を示します。

      f:id:ksby:20151123162509p:plain

    • 単に表示するだけならば status variable は使用しません。th:each で定義したオブジェクト名 + "." + フィールド名で出力できます。
  • Validator クラスで特定の行の入力フィールドにエラーをセットしたい場合には、errors.rejectValue(String.format("lendingBookDtoList[%d].lendingAppReason", i), null); のように指定します。詳細は今回作成した LendingappFormValidator#validate メソッドを見てください。

ソースコード

ExceptionHandlerAdvice.java

■その1

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.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;
        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());
        } 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;
    }

}
  • request.getRequestURI() で取得した URI が "/error" の場合には request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI) で元の URI を取得するようにします。
  • URL パラメータはどちらの場合も request.getQueryString() で取得できるようなので、URL/URI 取得処理とは別にします。

■その2

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

}
  • スタックトレースを出力する処理を StringWriter, PrintWriter を使用する方法に修正します。
  • handleException メソッドthrows IOException を追加します。

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;
import java.io.IOException;

@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)
            throws IOException {
        ModelAndView model = exceptionHandlerAdvice.handleException(e, request, response);
        return model;
    }
    
}
  • index メソッドthrows IOException を追加します。

LendingBookDto.java

package ksbysample.webapp.lending.web.lendingapp;

import ksbysample.webapp.lending.entity.LendingBook;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.BeanUtils;

import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

@Data
@NoArgsConstructor
public class LendingBookDto {

    private Long lendingBookId;

    String isbn;

    String bookName;

    String lendingState;

    @Pattern(regexp = "^(|1)$")
    String lendingAppFlg;

    @Size(max = 128)
    String lendingAppReason;

    LendingBookDto(LendingBook lendingBook) {
        BeanUtils.copyProperties(lendingBook, this);
    }

}

LendingappForm.java

package ksbysample.webapp.lending.web.lendingapp;

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

import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;

@Data
public class LendingappForm {

    private LendingApp lendingApp;

    @Valid
    private List<LendingBookDto> lendingBookDtoList;

    public void setLendingBookList(List<LendingBook> lendingBookList) {
        this.lendingBookDtoList = new ArrayList<>();
        lendingBookList.stream()
                .forEach(lendingBook -> {
                    LendingBookDto lendingBookDto = new LendingBookDto(lendingBook);
                    this.lendingBookDtoList.add(lendingBookDto);
                });
    }

}
  • private List<LendingBook> lendingBookList;private List<LendingBookDto> lendingBookDtoList; へ変更します。
  • lendingBookDtoList には @Valid アノテーションを付加します。Controller クラスで @Validated アノテーションが付加されて Bean Validation が実行される場合に LendingBookDto クラスの Bean Validation も実行されるようにするためです。
  • Entity クラスのリストから Dto クラスのリストへデータをコピーするための setLendingBookList メソッドを追加します。

messages_ja_JP.properties

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

LendingappForm.lendingAppId.emptyerr=貸出申請IDが指定されていません。
LendingappForm.lendingApp.nodataerr=指定された貸出申請IDのデータが登録されておりません。
LendingappForm.lendingBookDtoList.notExistApply=最低1冊「申請する」を選択してください。
LendingappForm.lendingBookDtoList.emptyReason=「申請する」を選択している書籍の申請理由を入力してください。
  • LendingappForm.lendingBookDtoList.notExistApply, LendingappForm.lendingBookDtoList.emptyReason の2つを追加します。

LendingappFormValidator.java

package ksbysample.webapp.lending.web.lendingapp;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import static ksbysample.webapp.lending.values.LendingBookLendingAppFlgValues.APPLY;

@Component
public class LendingappFormValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.equals(LendingappForm.class);
    }

    @Override
    public void validate(Object target, Errors errors) {
        LendingappForm lendingappForm = (LendingappForm) target;

        // 以下の点をチェックする
        // ・最低1つ「申請する」が選択されているか
        // ・「申請する」が選択されている場合に申請理由が入力されているか
        boolean existApply = false;
        boolean rejectEmptyReason = false;
        int i = 0;
        for (LendingBookDto lendingBookDto : lendingappForm.getLendingBookDtoList()) {
            if (StringUtils.equals(lendingBookDto.getLendingAppFlg(), APPLY.getValue())) {
                existApply = true;

                if (StringUtils.isBlank(lendingBookDto.getLendingAppReason())) {
                    errors.rejectValue(String.format("lendingBookDtoList[%d].lendingAppReason", i), null);
                    if (!rejectEmptyReason) {
                        errors.reject("LendingappForm.lendingBookDtoList.emptyReason", null);
                        rejectEmptyReason = true;
                    }
                }
            }
            i++;
        }

        if (!existApply) {
            i = 0;
            for (LendingBookDto lendingBookDto : lendingappForm.getLendingBookDtoList()) {
                errors.rejectValue(String.format("lendingBookDtoList[%d].lendingAppFlg", i), null);
                i++;
            }
            errors.reject("LendingappForm.lendingBookDtoList.notExistApply", null);
        }
    }

}

LendingBookDao.java

package ksbysample.webapp.lending.dao;

import ksbysample.webapp.lending.entity.LendingBook;
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 org.seasar.doma.jdbc.SelectOptions;

import java.util.List;

/**
 */
@Dao
@ComponentAndAutowiredDomaConfig
public interface LendingBookDao {

    /**
     * @param lendingBookId
     * @return the LendingBook entity
     */
    @Select
    LendingBook selectById(Long lendingBookId);
    @Select
    List<LendingBook> selectByLendingAppId(Long lendingAppId);
    @Select
    List<LendingBook> selectByLendingAppId(Long lendingAppId, SelectOptions options);
    
    /**
     * @param entity
     * @return affected rows
     */
    @Insert
    int insert(LendingBook entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Update
    int update(LendingBook entity);
    @Update(include = {"lendingState"})
    int updateLendingState(LendingBook entity);
    @Update(include = {"lendingAppFlg", "lendingAppReason"})
    int updateLendingAppFlgAndReason(LendingBook entity);
    
    /**
     * @param entity
     * @return affected rows
     */
    @Delete
    int delete(LendingBook entity);
}
  • @Update(include = {"lendingAppFlg", "lendingAppReason"}) int updateLendingAppFlgAndReason(LendingBook entity); を追加します。

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

    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) {
        // 更新対象のデータを取得する(ロックする)
        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);
                });
    }
    
}

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.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
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;

    @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) {
        if (bindingResultForLendingappParamForm.hasErrors()) {
            throw new WebApplicationRuntimeException(
                    messagesPropertiesHelper.getMessage("LendingappForm.lendingAppId.emptyerr", null));
        }

        // 画面に表示するデータを取得する
        setDispData(lendingappParamForm.getLendingAppId(), lendingappForm);

        return "lendingapp/lendingapp";
    }

    @RequestMapping(value = "/apply", method = RequestMethod.POST)
    public String apply(@Validated LendingappForm lendingappForm
            , BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "lendingapp/lendingapp";
        }

        // 入力された内容で申請する
        lendingappService.apply(lendingappForm);

        // 画面に表示するデータを取得する
        setDispData(lendingappForm.getLendingApp().getLendingAppId(), lendingappForm);
        
        return "lendingapp/lendingapp";
    }

    @RequestMapping(value = "/temporarySave", method = RequestMethod.POST)
    public String temporarySave() {
        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);
    }
    
}
  • Validator クラスの入力チェックが実行されるようにするために以下2点を追加します。
    • @Autowired private LendingappFormValidator lendingappFormValidator; を追加します。
    • @InitBinder(value = "lendingappForm") public void initBinder(WebDataBinder binder) { ... } を追加します。
  • apply メソッドでも画面に表示するデータを取得する処理を使用するので、取得用の共通メソッドを作成し、index メソッドはそのメソッドを呼び出すようにします。
  • apply メソッドを実装します。

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 getValue(String classSimpleName, String valueName)
            throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class<?> clazz = Class.forName(this.valuesObjList.get(classSimpleName));
        Method method = clazz.getMethod("valueOf", String.class);
        Values value = (Values) method.invoke(null, valueName);
        return value.getValue();
    }
    
    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;
    }
    
}
  • getValue メソッドを追加します。Thymeleaf テンプレートファイル内で ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')} のように使用します。

LendingAppStatusValues.java

package ksbysample.webapp.lending.values;

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

@Getter
public enum LendingAppStatusValues implements Values {

    TENPORARY_SAVE("1", "一時保存")
    , UNAPPLIED("2", "未申請")
    , PENDING("3", "申請中")
    , APPLOVED("4", "承認済");

    private final String value;
    private final String text;

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

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

        return result;
    }

}
  • implements 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;
        }
        .has-error {
            background-color: #ffcccc;
        }
    </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="callout callout-danger" th:if="${#fields.hasGlobalErrors()}">
                                <p th:each="err : ${#fields.globalErrors()}" th:text="${err}">共通エラーメッセージ表示エリア</p>
                            </div>
                            <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>
                                        <input type="hidden" th:field="*{lendingApp.lendingAppId}"/>
                                        <input type="hidden" th:field="*{lendingApp.status}"/>
                                    </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="lendingBookDto, iterStat : *{lendingBookDtoList}">
                                            <th th:text="${iterStat.count}">1</th>
                                            <th th:text="${lendingBookDto.isbn}">978-1-4302-5908-4</th>
                                            <th th:text="${lendingBookDto.bookName}">Spring Recipes</th>
                                            <th th:text="${lendingBookDto.lendingState}">蔵書なし</th>
                                            <th>
                                                <select class="form-control input-sm"
                                                        th:classappend="${#fields.hasErrors('*{lendingBookDtoList[__${iterStat.index}__].lendingAppFlg}')} ? 'has-error' : ''"
                                                        th:field="*{lendingBookDtoList[__${iterStat.index}__].lendingAppFlg}"
                                                        th:if="*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')} and ${lendingBookDto.lendingState} == '蔵書あり'">
                                                    <option th:each="lendingAppFlg : ${@vh.values('LendingBookLendingAppFlgValues')}"
                                                            th:value="${lendingAppFlg.getValue()}"
                                                            th:text="${lendingAppFlg.getText()}">しない</option>
                                                </select>
                                                <span th:text="${@vh.getText('LendingBookLendingAppFlgValues', lendingBookDto.lendingAppFlg)}"
                                                      th:if="*{lendingApp.status} != ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')}">しない
                                                </span>
                                            </th>
                                            <th>
                                                <input type="text" class="form-control input-sm"
                                                       th:classappend="${#fields.hasErrors('*{lendingBookDtoList[__${iterStat.index}__].lendingAppReason}')} ? 'has-error' : ''"
                                                       th:field="*{lendingBookDtoList[__${iterStat.index}__].lendingAppReason}"
                                                       th:if="*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')} and ${lendingBookDto.lendingState} == '蔵書あり'"/>
                                                <span th:text="${lendingBookDto.lendingAppReason}"
                                                      th:if="*{lendingApp.status} != ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')}">入力された申請理由を表示する
                                                </span>
                                            </th>
                                            <input type="hidden" th:field="*{lendingBookDtoList[__${iterStat.index}__].lendingBookId}"/>
                                            <input type="hidden" th:field="*{lendingBookDtoList[__${iterStat.index}__].isbn}"/>
                                            <input type="hidden" th:field="*{lendingBookDtoList[__${iterStat.index}__].bookName}"/>
                                            <input type="hidden" th:field="*{lendingBookDtoList[__${iterStat.index}__].lendingState}"/>
                                        </tr>
                                        </tbody>
                                    </table>
                                    <div class="text-center" th:if="*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')}">
                                        <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>
  • .has-error { ... } を追加します。
  • エラーメッセージ表示エリアとして <div class="callout callout-danger" th:if="${#fields.hasGlobalErrors()}"> ... </div> を追加します。
  • 以下の2行を追加します。
    • <input type="hidden" th:field="*{lendingApp.lendingAppId}"/>
    • <input type="hidden" th:field="*{lendingApp.status}"/>
  • <tr th:each="lendingBook, iterStat : *{lendingBookList}"><tr th:each="lendingBookDto, iterStat : *{lendingBookDtoList}"> へ変更します。またこの変更に伴い、ループ処理内の lendingBooklendingBookDto へ変更します。
  • 「申請する/しない」の select タグの以下の点を変更します。
    • th:classappend="${#fields.hasErrors('*{lendingBookDtoList[__${iterStat.index}__].lendingAppFlg}')} ? 'has-error' : ''" を追加します。
    • th:if*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')} and の条件を追加します。
  • 「申請する/しない」を申請後にはテキスト表示させるために <span th:text="${@vh.getText('LendingBookLendingAppFlgValues', lendingBookDto.lendingAppFlg)}" ...> ... </span> を追加します。
  • 「申請理由」の input タグの以下の点を変更します。
    • th:classappend="${#fields.hasErrors('*{lendingBookDtoList[__${iterStat.index}__].lendingAppReason}')} ? 'has-error' : ''" を追加します。
    • th:if*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')} and の条件を追加します。
  • 「申請理由」を申請後にはテキスト表示させるために <span th:text="${lendingBookDto.lendingAppReason}" ...> ... </span> を追加します。
  • 以下の4行を追加します。
    • <input type="hidden" th:field="*{lendingBookDtoList[__${iterStat.index}__].lendingBookId}"/>
    • <input type="hidden" th:field="*{lendingBookDtoList[__${iterStat.index}__].isbn}"/>
    • <input type="hidden" th:field="*{lendingBookDtoList[__${iterStat.index}__].bookName}"/>
    • <input type="hidden" th:field="*{lendingBookDtoList[__${iterStat.index}__].lendingState}"/>
  • 「申請」「一時保存」ボタンを囲む div タブに th:if="*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')}" を追加します。

履歴

2015/11/23
初版発行。