かんがるーさんの日記

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

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その17 )( 入力画面1を作成する )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その16 )( H2 Database に Flyway でテーブルを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • build すると Checkstyle, FindBugs, PMD が警告を出したので対応する
    • 入力画面1の作成

参照したサイト・書籍

目次

  1. build したら Checkstyle, FindBugs, PMD が警告を出したので対応する
    1. Javadoc タグには空でない説明文が必要です。
    2. Cannot open codebase filesystem:C:\project-springboot\ksbysample-boot-miscellaneous\boot-npm-geb-sample\build\classes\main\db\migration\V1__init.sql
    3. Too many fields
  2. input01.html 内の input タグに maxlength 属性を追加する
  3. Javascript 実装前に npm run springboot と Tomcat を起動して入力画面1を表示しようとしたら画面が表示されませんでした。。。
  4. 性別、職業の選択肢を表示する処理を追加する
  5. 続く。。。

手順

build したら Checkstyle, FindBugs, PMD が警告を出したので対応する

前回までの対応の後に build していなかったので build してみたところ、Checkstyle, FindBugs, PMD が警告を出したので対応します。

Javadoc タグには空でない説明文が必要です。

Doma-Gen で自動生成した Dao インターフェース src/main/java/ksbysample/webapp/bootnpmgeb/dao/InquiryDataDao.javaJavadoc のコメントで @param の説明文が空だったので警告が出ていました。

config/checkstyle/google_checks.xml に定義している module の <module name="NonEmptyAtclauseDescription"> ... </module> によるチェックの結果のようです。

Doma-Gen で自動生成する時に自動でパラメータ名と同じ文字列を説明文に記述するようにします。build.gradle の domaGen タスクを以下のように変更します。

// for Doma-Gen
task domaGen {
    doLast {
        ..........

        // 生成された Dao インターフェースを作業用ディレクトリにコピーし、
        // @ComponentAndAutowiredDomaConfig アノテーションを付加し、
        // Javadoc の @param に説明文を追加する
        copy() {
            from "${daoPackagePath}"
            into "${workDaoDirPath}/replace"
            filter {
                line ->
                    line.replaceAll('import org.seasar.doma.Dao;', "import ${importOfComponentAndAutowiredDomaConfig};\nimport org.seasar.doma.Dao;")
                            .replaceAll('@Dao', '@Dao\n@ComponentAndAutowiredDomaConfig')
                            .replaceAll('@param (\\S+)$', '@param $1 $1')
            }
        }

        ..........
    }
}
  • .replaceAll('@param (\\S+)$', '@param $1 $1') を追加します。

InquiryDataDao.java と InquiryData.java を削除してから Tomcat を起動し、domaGen タスクを実行します。

自動生成された src/main/java/ksbysample/webapp/bootnpmgeb/dao/InquiryDataDao.java を見ると、以下のようにパラメータ名と同じ文字列がセットされています。

@Dao
@ComponentAndAutowiredDomaConfig
public interface InquiryDataDao {

    /**
     * @param id id
     * @return the InquiryData entity
     */
    @Select
    InquiryData selectById(Integer id);

    ..........

Cannot open codebase filesystem:C:\project-springboot\ksbysample-boot-miscellaneous\boot-npm-geb-sample\build\classes\main\db\migration\V1__init.sql

build すると Flyway 用に追加した V1__init.sql が build/classes/main/db/migration/V1__init.sql にコピーされるのですが、build.gradle の tasks.withType(FindBugs) { ... } の中でこのファイルを FindBugs のチェック対象から除外していなかったため、以下のエラーが出ていました。

f:id:ksby:20170824000213p:plain

build.gradle の tasks.withType(FindBugs) { ... } を以下のように変更します。

tasks.withType(FindBugs) {
    // Gradle 3.3以降 + FindBugs Gradle Plugin を組み合わせると、"The following errors occurred during analysis:"
    // の後に "Cannot open codebase filesystem:..." というメッセージが大量に出力されるので、以下の doFirst { ... }
    // のコードを入れることで出力されないようにする
    doFirst {
        def fc = classes
        if (fc == null) {
            return
        }
        fc.exclude '**/*.properties'
        fc.exclude '**/*.sql'
        fc.exclude '**/*.xml'
        fc.exclude '**/META-INF/**'
        fc.exclude '**/static/**'
        fc.exclude '**/templates/**'
        classes = files(fc.files)
    }
    reports {
        xml.enabled = false
        html.enabled = true
    }
}
  • fc.exclude '**/*.sql' を追加します。

Too many fields

Doma-Gen で自動生成した Entity クラス src/main/java/ksbysample/webapp/bootnpmgeb/entity/InquiryData.java のフィールド数が多いので、PMD の <rule ref="rulesets/java/codesize.xml"> ... </rule> の TooManyFields で警告が出ていました。

Entity クラスを自動生成する時に @SuppressWarnings({"PMD.TooManyFields"}) を付加して PMD のチェック対象外になるようにします。build.gradle の domaGen タスクを以下のように変更します。

// for Doma-Gen
task domaGen {
    doLast {
        // まず変更が必要なもの
        def rootPackageName = 'ksbysample.webapp.bootnpmgeb'
        def rootPackagePath = 'src/main/java/ksbysample/webapp/bootnpmgeb'
        def dbUrl = 'jdbc:h2:tcp://localhost:9092/mem:bootnpmgebdb'
        def dbUser = 'sa'
        def dbPassword = ''
        def tableNamePattern = 'INQUIRY_DATA'
        // おそらく変更不要なもの
        def entityPackagePath = rootPackagePath + '/entity'
        def daoPackagePath = rootPackagePath + '/dao'
        def importOfComponentAndAutowiredDomaConfig = "${rootPackageName}.util.doma.ComponentAndAutowiredDomaConfig"
        def workDirPath = 'work'
        def workEntityDirPath = "${workDirPath}/entity"
        def workDaoDirPath = "${workDirPath}/dao"

        // 作業用ディレクトリを削除する
        clearDir("${workDirPath}")

        // 現在の Dao インターフェースのバックアップを取得する
        copy() {
            from "${daoPackagePath}"
            into "${workDaoDirPath}/org"
        }

        // Dao インターフェース、Entity クラスを生成する
        ant.taskdef(resource: 'domagentask.properties',
                classpath: configurations.domaGenRuntime.asPath)
        ant.gen(url: "${dbUrl}", user: "${dbUser}", password: "${dbPassword}", tableNamePattern: "${tableNamePattern}") {
            entityConfig(packageName: "${rootPackageName}.entity", useListener: false)
            daoConfig(packageName: "${rootPackageName}.dao")
            sqlConfig()
        }

        // 生成された Entity クラスを作業用ディレクトリにコピーし、
        // @SuppressWarnings({"PMD.TooManyFields"}) アノテーションを付加する
        copy() {
            from "${entityPackagePath}"
            into "${workEntityDirPath}/replace"
            filter {
                line ->
                    line.replaceAll('@Entity', '@SuppressWarnings({"PMD.TooManyFields"})\n@Entity')
            }
        }

        // @SuppressWarnings({"PMD.TooManyFields"}) アノテーションを付加した Entity クラスを
        // entity パッケージへ戻す
        copy() {
            from "${workEntityDirPath}/replace"
            into "${entityPackagePath}"
        }

        ..........
  • 以下の3行を追加します。
    • def rootPackagePath = 'src/main/java/ksbysample/webapp/bootnpmgeb'
    • def entityPackagePath = rootPackagePath + '/entity'
    • def workEntityDirPath = "${workDirPath}/entity"
  • daoPackagePath は rootPackagePath を使うよう変更し、記述する位置を // おそらく変更不要なもの の下へ移動します。
  • copy() { from "${entityPackagePath}" ... } を追加します。
  • copy() { from "${workEntityDirPath}/replace" ... } を追加します。

再度 InquiryDataDao.java と InquiryData.java を削除してから Tomcat を起動し直し、domaGen タスクを実行します。

自動生成された src/main/java/ksbysample/webapp/bootnpmgeb/entity/InquiryData.java を見ると、@SuppressWarnings({"PMD.TooManyFields"}) アノテーションが付加されています。

@SuppressWarnings({"PMD.TooManyFields"})
@Entity
@Table(name = "INQUIRY_DATA")
public class InquiryData {

以上の3点の警告を対応した後に build し直すと、今度は何も警告は表示されませんでした。

input01.html 内の input タグに maxlength 属性を追加する

INQUIRY_DATA テーブルのカラムに定義した文字数を src/main/resources/templates/web/inquiry/input01.html 内の <input type="text" .../> に maxlength 属性として追加します。

Javascript 実装前に npm run springbootTomcat を起動して入力画面1を表示しようとしたら画面が表示されませんでした。。。

Javascript の実装前にコマンドラインから npm run springboot を実行し、Tomcat を起動します。

ブラウザから http://localhost:9080/inquiry/input/01/ にアクセスして入力画面1が表示されることを確認しようとしたら、画面が表示されませんでした。。。

f:id:ksby:20170823074738p:plain

CSRF 絡みですね。おそらく H2 Console を表示するために設定を変更したのが原因でしょう。

調べたら原因は http.csrf().requireCsrfProtectionMatcher(...)CSRF対策の対象にしない URL を設定する時には一緒に GET オプション等で CSRF対策が実行されないよう設定しないといけないからでした。Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その13 )( Spring Session を使用する2 ) で書いていたのに忘れていました。

src/main/java/ksbysample/webapp/bootnpmgeb/config/WebSecurityConfig.java を以下のように変更します。

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private static final Pattern DISABLE_CSRF_TOKEN_PATTERN = Pattern.compile("(?i)^(GET|HEAD|TRACE|OPTIONS)$");
    private static final Pattern H2_CONSOLE_URI_PATTERN = Pattern.compile("^/h2-console");

    ..........

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ..........

        // spring.h2.console.enabled=true に設定されている場合には H2 Console を表示するために必要な設定を行う
        if (springH2ConsoleEnabled) {
            http.csrf()
                    .requireCsrfProtectionMatcher(request -> {
                        if (DISABLE_CSRF_TOKEN_PATTERN.matcher(request.getMethod()).matches()) {
                            // GET, HEAD, TRACE, OPTIONS は CSRF対策の対象外にする
                            return false;
                        } else if (H2_CONSOLE_URI_PATTERN.matcher(request.getRequestURI()).lookingAt()) {
                            // H2 Console は CSRF対策の対象外にする
                            return false;
                        }
                        return true;
                    });
            http.headers().frameOptions().sameOrigin();
        }
    }

}
  • private static final Pattern DISABLE_CSRF_TOKEN_PATTERN = Pattern.compile("(?i)^(GET|HEAD|TRACE|OPTIONS)$"); を追加します。
  • if (DISABLE_CSRF_TOKEN_PATTERN.matcher(request.getMethod()).matches()) { ... } を追加し、その次の if 文を else if に変更します。

Tomcat を再起動して http://localhost:9080/inquiry/input/01/ にアクセスすると今度は入力画面1が表示されました。

f:id:ksby:20170823080947p:plain

性別、職業の選択肢を表示する処理を追加する

src/main/java/ksbysample/webapp/bootnpmgeb の下に values パッケージを新規作成した後、ksbysample-webapp-lending から以下のファイルをコピーします。

  • src/main/java/ksbysample/webapp/bootnpmgeb/values/Values.java
  • src/main/java/ksbysample/webapp/bootnpmgeb/values/ValuesHelper.java
  • src/main/java/ksbysample/webapp/bootnpmgeb/values/validation/ValuesEnum.java
  • src/main/java/ksbysample/webapp/bootnpmgeb/values/validation/ValuesEnumValidator.java

src/main/resources/application-product.properties を以下のように変更します。

..........

valueshelper.classpath.prefix=BOOT-INF.classes.

  • ファイルの末尾に valueshelper.classpath.prefix=BOOT-INF.classes. を追加します。

まずは「性別」からです。src/main/java/ksbysample/webapp/bootnpmgeb/values の下に SexValues.java を新規作成し、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.values;

import lombok.AllArgsConstructor;
import lombok.Getter;

@SuppressWarnings("MissingOverride")
@Getter
@AllArgsConstructor
public enum SexValues implements Values {

    MALE("1", "男性")
    , FEMALE("2", "女性");

    private final String value;
    private final String text;

}

src/main/resources/templates/web/inquiry/input01.html の「性別」の部分を以下のように変更します。

            <!-- 性別 -->
            <div class="form-group" id="form-group-sex">
              <div class="control-label col-sm-2">
                <label class="float-label">性別</label>
                <div class="label label-required">必須</div>
              </div>
              <div class="col-sm-10">
                <div class="row"><div class="col-sm-10">
                  <div class="radio-inline" th:each="sexValue : ${@vh.values('SexValues')}">
                    <label><input type="radio" name="sex" th:value="${sexValue.value}" th:text="${sexValue.text}"></label>
                  </div>
                </div></div>
                <div class="row hidden js-errmsg"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>
              </div>
            </div>
  • <div class="radio-inline" ...>th:each="sexValue : ${@vh.values('SexValues')}" を追加します。
  • ラジオボタンの各選択肢を記述していた HTML を <label><input type="radio" name="sex" th:value="${sexValue.value}" th:text="${sexValue.text}"></label> に変更します。

次は「職業」です。src/main/java/ksbysample/webapp/bootnpmgeb/values の下に JobValues.java を新規作成し、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.values;

import lombok.AllArgsConstructor;
import lombok.Getter;

@SuppressWarnings("MissingOverride")
@Getter
@AllArgsConstructor
public enum JobValues implements Values {

    EMPLOYEE("1", "会社員")
    , STUDENT("2", "学生")
    , OTHER("3", "その他");

    private final String value;
    private final String text;

}

src/main/resources/templates/web/inquiry/input01.html の「職業」の部分を以下のように変更します。

            <!-- 職業 -->
            <div class="form-group" id="form-group-job">
              <div class="control-label col-sm-2">
                <label class="float-label">職業</label>
              </div>
              <div class="col-sm-10">
                <div class="row"><div class="col-sm-10">
                  <select name="job" id="job" class="form-control" style="width: 250px;">
                    <th:block th:each="jobValue,iterStat : ${@vh.values('JobValues')}">
                    <option th:if="${iterStat.first}" value="">選択してください</option>
                    <option th:value="${jobValue.value}" th:text="${jobValue.text}">会社員</option>
                    </th:block>
                  </select>
                </div></div>
                <div class="row hidden js-errmsg"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>
              </div>
            </div>
  • 以下の3行を追加します。
    • <th:block th:each="jobValue,iterStat : ${@vh.values('JobValues')}">
    • <option th:if="${iterStat.first}" value="">選択してください</option>
    • </th:block>
  • <option>th:value="${jobValue.value}" th:text="${jobValue.text}" を追加します。

画面を表示すると「性別」「職業」どちらも問題なく表示されています。

f:id:ksby:20170826004452p:plain

続く。。。

記事が長くなったので、あと1~2回くらいに分けて書きます。

最後に結構大きく修正した build.gradle の domaGen タスクを載せておきます。

ソースコード

build.gradle

// for Doma-Gen
task domaGen {
    doLast {
        // まず変更が必要なもの
        def rootPackageName = 'ksbysample.webapp.bootnpmgeb'
        def rootPackagePath = 'src/main/java/ksbysample/webapp/bootnpmgeb'
        def dbUrl = 'jdbc:h2:tcp://localhost:9092/mem:bootnpmgebdb'
        def dbUser = 'sa'
        def dbPassword = ''
        def tableNamePattern = 'INQUIRY_DATA'
        // おそらく変更不要なもの
        def entityPackagePath = rootPackagePath + '/entity'
        def daoPackagePath = rootPackagePath + '/dao'
        def importOfComponentAndAutowiredDomaConfig = "${rootPackageName}.util.doma.ComponentAndAutowiredDomaConfig"
        def workDirPath = 'work'
        def workEntityDirPath = "${workDirPath}/entity"
        def workDaoDirPath = "${workDirPath}/dao"

        // 作業用ディレクトリを削除する
        clearDir("${workDirPath}")

        // 現在の Dao インターフェースのバックアップを取得する
        copy() {
            from "${daoPackagePath}"
            into "${workDaoDirPath}/org"
        }

        // Dao インターフェース、Entity クラスを生成する
        ant.taskdef(resource: 'domagentask.properties',
                classpath: configurations.domaGenRuntime.asPath)
        ant.gen(url: "${dbUrl}", user: "${dbUser}", password: "${dbPassword}", tableNamePattern: "${tableNamePattern}") {
            entityConfig(packageName: "${rootPackageName}.entity", useListener: false)
            daoConfig(packageName: "${rootPackageName}.dao")
            sqlConfig()
        }

        // 生成された Entity クラスを作業用ディレクトリにコピーし、
        // @SuppressWarnings({"PMD.TooManyFields"}) アノテーションを付加する
        copy() {
            from "${entityPackagePath}"
            into "${workEntityDirPath}/replace"
            filter {
                line ->
                    line.replaceAll('@Entity', '@SuppressWarnings({"PMD.TooManyFields"})\n@Entity')
            }
        }

        // @SuppressWarnings({"PMD.TooManyFields"}) アノテーションを付加した Entity クラスを
        // entity パッケージへ戻す
        copy() {
            from "${workEntityDirPath}/replace"
            into "${entityPackagePath}"
        }

        // 生成された Dao インターフェースを作業用ディレクトリにコピーし、
        // @ComponentAndAutowiredDomaConfig アノテーションを付加し、
        // Javadoc の @param に説明文を追加する
        copy() {
            from "${daoPackagePath}"
            into "${workDaoDirPath}/replace"
            filter {
                line ->
                    line.replaceAll('import org.seasar.doma.Dao;', "import ${importOfComponentAndAutowiredDomaConfig};\nimport org.seasar.doma.Dao;")
                            .replaceAll('@Dao', '@Dao\n@ComponentAndAutowiredDomaConfig')
                            .replaceAll('@param (\\S+)$', '@param $1 $1')
            }
        }

        // @ComponentAndAutowiredDomaConfig アノテーションを付加した Dao インターフェースを
        // dao パッケージへ戻す
        copy() {
            from "${workDaoDirPath}/replace"
            into "${daoPackagePath}"
        }

        // 元々 dao パッケージ内にあったファイルを元に戻す
        copy() {
            from "${workDaoDirPath}/org"
            into "${daoPackagePath}"
        }

        // 作業用ディレクトリを削除する
        clearDir("${workDirPath}")
    }
}

履歴

2017/08/26
初版発行。