かんがるーさんの日記

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

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その6 )( Thymeleaf を 2.1.5 → 3.0.6 へバージョンアップする2 )

概要

記事一覧はこちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その5 )( Thymeleaf を 2.1.5 → 3.0.6 へバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 前回に続き Thymeleaf 3 へのバージョンアップを行います。

参照したサイト・書籍

  1. Read Input hidden tag value from url of website using Jsoup in java
    https://stackoverflow.com/questions/13071165/read-input-hidden-tag-value-from-url-of-website-using-jsoup-in-java

目次

  1. mainparts.html の head タグを th:fragment 化し、head-cssjs.html を削除する(その2)
  2. LibraryHelper#getSelectedLibrary を変更する
  3. 全ての各画面用 Thymeleaf テンプレートに <!--/*@thymesVar id="..." type="..."*/--> を記述する
  4. WARN ログ [THYMELEAF][main] Template Mode 'HTML5' is deprecated. Using Template Mode 'HTML' instead. が出力されないようにする
  5. clean タスク → Rebuild Project → build タスクを実行する
  6. メモ書き
    1. th:block は末尾に “/” が必ず必要
    2. .andExpect(...) は lambda 式で .andExpect(mvcResult -> {...}) と書けば AssertJ が利用できる

手順

mainparts.html の head タグを th:fragment 化し、head-cssjs.html を削除する(その2)

  1. 残りの各画面用 Thymeleaf テンプレートを変更します。ファイル数が多いので、変更内容は書きません。また全て src/main/resources の下のファイルです。

    • templates/booklist/booklist.html
    • templates/booklist/complete.html
    • templates/booklist/fileupload.html
    • templates/confirmresult/confirmresult.html
    • templates/lendingapp/lendingapp.html
    • templates/lendingapproval/lendingapproval.html
    • templates/sessionsample/confirm.html
    • templates/sessionsample/first.html
    • templates/sessionsample/next.html
    • templates/springmvcmemo/beanValidationGroup.html
    • templates/textareamemo/display.html
    • templates/textareamemo/index.html
    • templates/error.html
    • templates/login.html
    • templates/loginsuccess.html
  2. Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その26 )( jar ファイルを作成して動作確認する2 ) に記載した手順に従って画面を操作し、画面の表示がおかしなところがないことを確認します。

  3. テストで .andExpect(xpath(...)) を使用しているところを変更します。こちらも変更内容は書きません。全て src/test/java の下のファイルです。

    • ksbysample.webapp.lending.web.confirmresult.ConfirmresultControllerTest
    • ksbysample.webapp.lending.web.lendingapp.LendingappControllerTest
    • ksbysample.webapp.lending.web.lendingapproval.LendingapprovalControllerTest
  4. clean タスク → Rebuild Project → build タスクを実行して “BUILD SUCCESSFUL” が出力されることを確認します。

    f:id:ksby:20170524173206p:plain

LibraryHelper#getSelectedLibrary を変更する

  1. src/main/java/ksbysample/webapp/lending/helper/library/LibraryHelper.javaリンク先の内容 に変更します。

  2. src/test/java/ksbysample/webapp/lending/helper/library/LibraryHelperTest.javaリンク先の内容 に変更します。変更後、テストが成功することを確認します。

    f:id:ksby:20170524201500p:plain

  3. src/main/resources/templates/common/mainparts.html を リンク先の内容 に変更します。

  4. Tomcat を起動して、図書館未選択時に “※図書館が選択されていません"、選択時に "選択中:…” が表示されることを確認します(画面キャプチャは省略します)。

全ての各画面用 Thymeleaf テンプレートに <!--/*@thymesVar id="..." type="..."*/--> を記述する

  1. 以下の Thymeleaf テンプレートに <!--/*@thymesVar id="..." type="..."*/--> を追加します。変更内容は書きません。全て src/main/resources の下のファイルです。

    • templates/admin/library/library.html
    • templates/booklist/booklist.html
    • templates/booklist/fileupload.html
    • templates/confirmresult/confirmresult.html
    • templates/lendingapp/lendingapp.html
    • templates/lendingapproval/lendingapproval.html

    <!--/*@thymesVar id="..." type="..."*/--> の記述は IntelliJ IDEA だと以下の操作で自動挿入できます。th:object 等で書いている変数の <!--/*@thymesVar id="..." type="..."*/--> の記述がないと、以下の画像のように赤波線が表示されます。

    f:id:ksby:20170524205303p:plain

    赤波線が表示されている変数にカーソルを移動して Alt+Enter を押すとコンテキストメニューが表示されますので、「Declare external variable in comment annotation」を選択します。

    f:id:ksby:20170524205517p:plain

    <!--/*@thymesVar id="..." type="..."*/--> の記述が自動で挿入されますので、あとは type の部分を記入します。

    f:id:ksby:20170524205810p:plain

WARN ログ [THYMELEAF][main] Template Mode 'HTML5' is deprecated. Using Template Mode 'HTML' instead. が出力されないようにする

  1. src/main/resources/application.properties を リンク先の内容 に変更します。

clean タスク → Rebuild Project → build タスクを実行する

  1. 最後に clean タスク → Rebuild Project → build タスクを実行して、ここまでの変更で問題がないことを確認します。

    f:id:ksby:20170525001743p:plain

メモ書き

th:block は末尾に “/” が必ず必要

今回 src/main/resources/templates/common/mainparts.html を修正した時に、Thymeleaf 3 から末尾の “/” は不要になったと考えて、最初 <th:block th:replace="${links} ?: _"> と末尾に “/” を付けずに書いたのですが、これだときちんと動作しませんでした。

例えば「貸出希望書籍 CSV ファイルアップロード」画面(http://localhost:8080/booklist)を表示すると、<th:block th:replace="${links} ?: _"/> の時は、HTML は以下のように出力されますが、

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>貸出希望書籍 CSV ファイルアップロード</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">
    <!-- Bootstrap 3.3.4 -->
    <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css">
    <!-- Font Awesome Icons -->
    <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css">
    <!-- Ionicons -->
    <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css">
    <!-- Theme style -->
    <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css">
    <!-- AdminLTE Skins. Choose a skin from the css/skins
         folder instead of downloading all of them to reduce the load. -->
    <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css">

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
    <script src="/js/html5shiv.min.js"></script>
    <script src="/js/respond.min.js"></script>
    <![endif]-->

    <!-- ここに各htmlで定義された link タグが追加される -->
    <link href="/css/fileinput.min.css" rel="stylesheet" type="text/css">

    <style type="text/css">
        <!--
        .jp-gothic {
            font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
        }
        .content-wrapper {
            background-color: #fffafa;
        }
        .noselected-library {
            color: #ff8679 !important;
            font-size: 100%;
            font-weight: 700;
        }
        .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%;
        }
        .has-error {
            background-color: #ffcccc;
        }
        -->
    </style>

    <!-- ここに各htmlで定義された style タグが追加される -->
    <style type="text/css">
        <!--
        .callout ul li {
            margin-left: -30px;
        }
        -->
    </style>
</head>

<th:block th:replace="${links} ?: _"> のように末尾の “/” を付け忘れると、<style type="text/css"> から下が表示されなくなります。しかも何のエラーも出ません(画面の背景色が青くなったのでおかしいことに気づきました)。

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>貸出希望書籍 CSV ファイルアップロード</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">
    <!-- Bootstrap 3.3.4 -->
    <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css">
    <!-- Font Awesome Icons -->
    <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css">
    <!-- Ionicons -->
    <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css">
    <!-- Theme style -->
    <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css">
    <!-- AdminLTE Skins. Choose a skin from the css/skins
         folder instead of downloading all of them to reduce the load. -->
    <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css">

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
    <script src="/js/html5shiv.min.js"></script>
    <script src="/js/respond.min.js"></script>
    <![endif]-->

    <!-- ここに各htmlで定義された link タグが追加される -->
    <link href="/css/fileinput.min.css" rel="stylesheet" type="text/css"></head>

HTML の meta タグ等は末尾を /> にする必要はなくなりましたが、th:block<th:block ... /> のように必ず末尾に “/” を付けましょう。

.andExpect(...) は lambda 式で .andExpect(mvcResult -> {...}) と書けば AssertJ が利用できる

今回久しぶりに ResultMatcher のクラスを作成して気づきましたが、.andExpect(...) 内に指定する式は、

  • MvcResult 型の引数を1つ渡して、戻り値が void 型の lambda 式でよい。
  • lambda 式の中で AssertionError の例外が throw されればテストが失敗する。

ということに気づきました(org.springframework.test.web.servlet.ResultMatcher インターフェース参照)。

ということは、.andExpect(mvcResult -> {...}) の形式で書けば、AssertJ を利用することができます。例えば、src/test/java/ksbysample/webapp/lending/web/lendingapproval/LendingapprovalControllerTest.javalendingAppIdパラメータで指定されたデータが登録されており承認権限を持つユーザならば貸出承認画面が表示される() メソッドを以下のように変更して、

        @Test
        @TestData("web/lendingapproval/testdata/001")
        public void lendingAppIdパラメータで指定されたデータが登録されており承認権限を持つユーザならば貸出承認画面が表示される() throws Exception {
            mvc.authSuzukiHanako.perform(get("/lendingapproval?lendingAppId=105"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(view().name("lendingapproval/lendingapproval"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(html("#lendingapprovalForm > div > div > table > tbody > tr").count(3))
                    .andExpect(mvcResult -> {
                        assertThat(Jsoup.parse(mvcResult.getResponse().getContentAsString())
                                .select("#lendingapprovalForm > div > div > table > tbody > tr > td:eq(1)"))
                                .extracting(Element::text)
                                .containsOnly("978-4-7741-5377-3", "978-4-7973-4778-4", "978-4-87311-704-1");
                    });
        }

テストを実行すると、問題なく成功します。

f:id:ksby:20170525010744p:plain

.containsOnly(...) に記述する要素を1つ削除してテストが失敗するように変更すると、

        @Test
        @TestData("web/lendingapproval/testdata/001")
        public void lendingAppIdパラメータで指定されたデータが登録されており承認権限を持つユーザならば貸出承認画面が表示される() throws Exception {
            mvc.authSuzukiHanako.perform(get("/lendingapproval?lendingAppId=105"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(view().name("lendingapproval/lendingapproval"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(html("#lendingapprovalForm > div > div > table > tbody > tr").count(3))
                    .andExpect(mvcResult -> {
                        assertThat(Jsoup.parse(mvcResult.getResponse().getContentAsString())
                                .select("#lendingapprovalForm > div > div > table > tbody > tr > td:eq(1)"))
                                .extracting(Element::text)
                                .containsOnly("978-4-7741-5377-3", "978-4-7973-4778-4");
                    });
        }

テストが失敗して、かつ失敗の原因が表示されます。

f:id:ksby:20170525011022p:plain

.andExpect(...) の中で AssertJ を使うことは可能です、という話でした。

ソースコード

LibraryHelper.java

    public String getSelectedLibrary() {
        String result = null;
        LibraryForsearch libraryForsearch = libraryForsearchDao.selectSelectedLibrary();
        if (libraryForsearch != null) {
            result = "選択中:" + libraryForsearch.getFormal();
        }
        return result;
    }
  • getSelectedLibrary メソッドで図書館が選択されていない場合には “※図書館が選択されていません” ではなく null を返すようにします。

LibraryHelperTest.java

    @Test
    public void testGetSelectedLibrary_図書館が選択されていない場合() throws Exception {
        given(libraryForsearchDao.selectSelectedLibrary()).willReturn(null);

        String result = libraryHelper.getSelectedLibrary();
        assertThat(result).isNull();
    }
  • testGetSelectedLibrary_図書館が選択されていない場合() テストメソッド内の assertThat(result).isEqualTo("※図書館が選択されていません");assertThat(result).isNull(); ヘ変更します。

mainparts.html

                <!-- Navbar Right Menu -->
                <div class="navbar-custom-menu">
                    <p class="navbar-text"
                       th:with="selectedLibrary=${@libraryHelper.getSelectedLibrary()}"
                       th:classappend="${selectedLibrary} ? 'selected-library' : 'noselected-library'"
                       th:text="${selectedLibrary} ?: _">※図書館が選択されていません</p>
                    <ul class="nav navbar-nav">
                        <li><a href="/logout">ログアウト</a></li>
                    </ul>
                </div>
  • th:with="selectedLibrary=${@libraryHelper.getSelectedLibrary()}" を追加します。
  • th:classappend の中の ${#strings.startsWith(@libraryHelper.getSelectedLibrary(), '※')}${selectedLibrary} に変更します。また 'noselected-library' : 'selected-library''selected-library' : 'noselected-library' に変更します。
  • th:text の中の ${@libraryHelper.getSelectedLibrary()}${selectedLibrary} ?: _ に変更します。

application.properties

spring.freemarker.cache=true
spring.freemarker.settings.number_format=computer
spring.freemarker.charset=UTF-8
spring.freemarker.enabled=false
spring.freemarker.prefer-file-system-access=false

spring.thymeleaf.mode=HTML

valueshelper.classpath.prefix=
  • spring.thymeleaf.mode=HTML を追加します。

履歴

2017/05/25
初版発行。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その5 )( Thymeleaf を 2.1.5 → 3.0.6 へバージョンアップする )

概要

記事一覧はこちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その4 )( 1.4系 → 1.5系で変更された点を修正する ) の続きです。

参照したサイト・書籍

  1. AttoParser
    http://www.attoparser.org/

  2. jsoup: Java HTML Parser
    https://jsoup.org/

  3. sagan/sagan-site/src/it/java/saganx/AuthenticationTests.java https://github.com/spring-io/sagan/blob/master/sagan-site/src/it/java/saganx/AuthenticationTests.java

    • Jsoup の使い方を参考にしました。
  4. Convert xPath to JSoup query
    http://stackoverflow.com/questions/16335820/convert-xpath-to-jsoup-query

  5. Groovyのヒアドキュメント
    http://d.hatena.ne.jp/Kazuhira/20130715/1373878511

目次

  1. 変更の方針
  2. build.gradle を変更する
  3. SpELコンパイラを有効にします
  4. mainparts.html の head タグを th:fragment 化し、head-cssjs.html を削除する(その1)
  5. ここで一旦動作確認する
  6. xpath() の代わりに使用できる HTML 5 チェック用の html() ResultMatcher を作成する
  7. 失敗したテストを変更する
  8. 続きます。。。

手順

変更の方針

  • Thymeleaf は 2.1.5 → 3.0.6 へバージョンアップします。Thymeleaf の関連ライブラリもバージョンアップします。
  • SpELコンパイラを有効にします。
  • mainparts.html の head タグに th:fragment を追記します。
  • head-cssjs.html の内容を mainparts.html の head タグ内にコピーし、head-cssjs.html は削除します。
  • mainparts.html の head タグ内の meta, link タグの末尾の “/” は削除します。
  • Fragment Expressions, The No-Operation token の機能を利用します。各画面用 Thymeleaf テンプレートの head タグを mainparts.html の head タグを使用するように変更し、各画面用 Thymeleaf テンプレートには画面固有のタグのみ記述して、そのタグが mainparts.html の head タグに反映されるようにします。
  • LibraryHelper#getSelectedLibrary では図書館が選択されている時だけ “選択中:…” の文字を返すようにし、"※図書館が選択されていません" の文字列は mainparts.html の head タグ内で The No-Operation token の機能を利用して表示するようにします。
  • 全ての各画面用 Thymeleaf テンプレートに <!--/* @thymesVar id="..." type="..." */--> が記述されていないので、form タグの上に追加します。

build.gradle を変更する

  1. build.gradle を リンク先のその1の内容 に変更します。

  2. 変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

    更新後、Project Tool Window の External Libraries を見て、ライブラリが更新されていることを確認します。

    f:id:ksby:20170521110748p:plain

BOM に記述されたライブラリのバージョンを変更する方法として今回は Dependency management plugin が提供する bomProperty を使用しましたが、それ以外に build.gradle に ext で記述する方法や、gradle.properties を使用する方法があります。詳細は以下の Web ページに記述されています。

SpELコンパイラを有効にします

  1. src/main/java/ksbysample/webapp/lending/config/WebMvcConfig.java を新規作成し、リンク先の内容 を記述します。

mainparts.html の head タグを th:fragment 化し、head-cssjs.html を削除する(その1)

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

  2. 各画面用 Thymeleaf テンプレートを1ファイルだけ変更します。src/main/resources/templates/admin/library/library.html を リンク先の内容 に変更します。

ここで一旦動作確認する

ここまでの変更で「検索対象図書館登録」画面は表示できるようになっているはずなので、確認してみます。

library_forsearch テーブルをクリアしてから bootRun で Tomcat を起動した後、http://localhost:8080/ にアクセスして tanaka.taro@sample.com / taro でログインすると「検索対象図書館登録」画面が表示されました。

f:id:ksby:20170521151522p:plain

HTML の <head> ... </head> タグは以下のように出力されており想定通りです。

<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">
    <!-- Bootstrap 3.3.4 -->
    <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css">
    <!-- Font Awesome Icons -->
    <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css">
    <!-- Ionicons -->
    <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css">
    <!-- Theme style -->
    <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css">
    <!-- AdminLTE Skins. Choose a skin from the css/skins
         folder instead of downloading all of them to reduce the load. -->
    <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css">

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
    <script src="/js/html5shiv.min.js"></script>
    <script src="/js/respond.min.js"></script>
    <![endif]-->

    <!-- ここに各htmlで定義された link タグが追加される -->
    

    <style type="text/css">
        <!--
        .jp-gothic {
            font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
        }
        .content-wrapper {
            background-color: #fffafa;
        }
        .noselected-library {
            color: #ff8679 !important;
            font-size: 100%;
            font-weight: 700;
        }
        .selected-library {
            color: #ffffff !important;
            font-size: 100%;
            font-weight: 700;
        }
        .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%;
        }
        -->
    </style>

    <!-- ここに各htmlで定義された style タグが追加される -->
    
</head>

Tomcat を停止します。

次に clean タスク → Rebuild Project → build タスクを実行してみます。

build タスクでいくつかのテストが失敗しました。

f:id:ksby:20170521152945p:plain

Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」も実行してみると、失敗しているテストで org.xml.sax.SAXParseException; lineNumber: 63; columnNumber: 3; 要素タイプ"link"は、対応する終了タグ"</link>"で終了する必要があります。 というメッセージが出力されていることが分かります。

f:id:ksby:20170521153512p:plain

Thymeleaf 3 にバージョンアップしたので SAX は使われなくなったのでは?と思ってテストのコードを見てみると、xpath を使用してチェックしていました。これが SAX を使用しているようです。

f:id:ksby:20170521154043p:plain

xpath() の代わりに使用できる HTML 5 チェック用の html() ResultMatcher を作成する

Thymeleaf 3 に合わせて HTML を XML 形式にしないように変更すると xpath() ResultMatcher が使用できなくなりました。HTML 5 をチェック可能な ResultMatcher を作成することにします。

Thymeleaf 3 ten-minute migration guide に書かれているリンク先の Fragment Expressions を見ると、Thymeleaf 3 で使用されている Parser が AttoParser というライブラリであることが分かります。

AttoParser のドキュメントや Thymeleaf の GitHub を見てみたのですが、ちょっと簡単に使えるライブラリではないように見えます。今回は jQuery と同じような CSS の指定で HTML の要素を取得することができる Jsoup を使用して ResultMatcher を作成することにします。

  1. build.gradle を リンク先のその2の内容 に変更します。

  2. 変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

  3. src/test/java/ksbysample/common/test/matcher/HtmlResultMatchers.java を新規作成し、リンク先の内容 を記述します。

  4. テストを作成します。src/groovy/test/ksbysample/common/test/matcher/HtmlResultMatchersTest.groovy を新規作成し、リンク先の内容 を記述します。

  5. 作成したテストを実行し、全てのテストが成功することを確認します。

    f:id:ksby:20170523101841p:plain f:id:ksby:20170523101952p:plain

失敗したテストを変更する

  1. src/test/java/ksbysample/webapp/lending/web/admin/library/AdminLibraryControllerTest.javaリンク先の内容 に変更します。

  2. テストを実行し、成功することを確認します。

    f:id:ksby:20170523160644p:plain

続きます。。。

1度調査しているのですぐに終わると思っていましたが、以外なところでつまずきました。一旦ここで区切ります。

ソースコード

build.gradle

■その1

dependencyManagement {
    imports {
        mavenBom("io.spring.platform:platform-bom:Brussels-SR2") {
            bomProperty 'guava.version', '21.0'
            bomProperty 'thymeleaf.version', '3.0.6.RELEASE'
            bomProperty 'thymeleaf-extras-springsecurity4.version', '3.0.2.RELEASE'
            bomProperty 'thymeleaf-layout-dialect.version', '2.2.2'
            bomProperty 'thymeleaf-extras-data-attribute.version', '2.0.1'
            bomProperty 'thymeleaf-extras-java8time.version', '3.0.0.RELEASE'
        }
    }
}
  • dependencyManagement に以下の記述を追加します。
    • bomProperty 'thymeleaf.version', '3.0.6.RELEASE'
    • bomProperty 'thymeleaf-extras-springsecurity4.version', '3.0.2.RELEASE'
    • bomProperty 'thymeleaf-layout-dialect.version', '2.2.2'
    • bomProperty 'thymeleaf-extras-data-attribute.version', '2.0.1'
    • bomProperty 'thymeleaf-extras-java8time.version', '3.0.0.RELEASE'

■その2

dependencies {
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    ..........
    testCompile("com.jayway.jsonpath:json-path:2.2.0")
    testCompile("org.jsoup:jsoup:1.10.2")
    testCompile("cglib:cglib-nodep:3.2.5")
    testCompile("org.spockframework:spock-core:${spockVersion}") {
    ..........
}
  • dependencies に以下の2行を追加します。cglib:cglib-nodep は Spock で Mock を使用するために必要となるライブラリです。指定しないとテスト実行時に org.spockframework.mock.CannotCreateMockException: Cannot create mock for class org.springframework.mock.web.MockHttpServletResponse. Mocking of non-interface types requires a code generation library. Please put byte-buddy-1.6.4 or cglib-nodep-3.2 or higher on the class path. というメッセージが出ます。
    • testCompile("org.jsoup:jsoup:1.10.2")
    • testCompile("cglib:cglib-nodep:3.2.5")

WebMvcConfig.java

package ksbysample.webapp.lending.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.SpringTemplateEngine;

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    /**
     * Thymeleaf 3 のパフォーマンスを向上させるために SpEL コンパイラを有効にする
     *
     * @param templateEngine {@link SpringTemplateEngine} オブジェクト
     */
    @Autowired
    public void configureThymeleafSpringTemplateEngine(SpringTemplateEngine templateEngine) {
        templateEngine.setEnableSpringELCompiler(true);
    }

}

common/mainparts.html

<head th:fragment="head(title, links, style)">
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title th:replace="${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">
    <!-- Bootstrap 3.3.4 -->
    <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css">
    <!-- Font Awesome Icons -->
    <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css">
    <!-- Ionicons -->
    <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css">
    <!-- Theme style -->
    <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css">
    <!-- AdminLTE Skins. Choose a skin from the css/skins
         folder instead of downloading all of them to reduce the load. -->
    <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css">

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
    <script src="/js/html5shiv.min.js"></script>
    <script src="/js/respond.min.js"></script>
    <![endif]-->

    <!-- ここに各htmlで定義された link タグが追加される -->
    <th:block th:replace="${links} ?: _"/>

    <style type="text/css">
        <!--
        .jp-gothic {
            font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
        }
        .content-wrapper {
            background-color: #fffafa;
        }
        .noselected-library {
            color: #ff8679 !important;
            font-size: 100%;
            font-weight: 700;
        }
        .selected-library {
            color: #ffffff !important;
            font-size: 100%;
            font-weight: 700;
        }
        .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%;
        }
        -->
    </style>

    <!-- ここに各htmlで定義された style タグが追加される -->
    <th:block th:replace="${style} ?: _"/>
</head>
  • head タグに th:fragment="head(title, links, style)" を追加します。
  • title タグに th:replace="${title} ?: _" を追加します。
  • <!-- Bootstrap 3.3.4 --> から </head> の前までの部分に src/main/resources/templates/common/head-cssjs.html の内容をコピーします。
  • <th:block th:replace="${links} ?: _"/> を追加します。
  • <th:block th:replace="${style} ?: _"/> を追加します。
  • meta, link タグの末尾の “/” を削除します。

admin/library/library.html

<head th:replace="~{common/mainparts :: head(~{::title}, ~{::link}, ~{::style})}">
    <title>検索対象図書館登録</title>
</head>
  • head タグに th:replace="~{common/mainparts :: head(~{::title}, ~{::link}, ~{::style})}" を追加します。
  • <head> ... </head> の中には <title>検索対象図書館登録</title> だけ記述します。

HtmlResultMatchers.java

package ksbysample.common.test.matcher;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultMatcher;

import java.io.UnsupportedEncodingException;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.util.AssertionErrors.assertTrue;

/**
 * Jsoup ( https://jsoup.org/ ) を利用して HTML のチェックをする
 * {@link org.springframework.test.web.servlet.ResultActions#andExpect(ResultMatcher)} で使用するためのクラス
 * <p>
 * <pre>{@code
 *      mockMvc.perform(get("/"))
 *          .andExpect(status().isOk())
 *          .andExpect(html("#id").text("ここに期待する文字列を記載する"));
 *          .andExpect(html("div > div:eq(1) > a:eq(1)").count(1));
 *          .andExpect(html(".css-selector").exists());
 * }</pre>
 */
public class HtmlResultMatchers {

    private String cssQuery;

    public HtmlResultMatchers(String cssQuery) {
        this.cssQuery = cssQuery;
    }

    /**
     * {@link HtmlResultMatchers} オブジェクト生成用の static メソッド
     *
     * @param cssQuery {@link org.jsoup.nodes.Element#select(String)} に指定する CSS セレクタ
     * @return {@link HtmlResultMatchers}
     */
    public static HtmlResultMatchers html(String cssQuery) {
        return new HtmlResultMatchers(cssQuery);
    }

    /**
     * HTML 内の cssQuery で指定された部分の文字列を抽出し、引数で渡された文字列と同じかチェックする
     *
     * @param expectedText 文字列の期待値
     * @return {@link ResultMatcher}
     */
    public ResultMatcher text(final String expectedText) {
        return mvcResult -> assertThat(selectFirst(mvcResult).text(), is(expectedText));
    }

    /**
     * HTML 内の cssQuery で指定された要素数を取得し、引数で渡された数と同じかチェックする
     *
     * @param expectedCount 要素数の期待値
     * @return {@link ResultMatcher}
     */
    public ResultMatcher count(final int expectedCount) {
        return mvcResult -> assertThat(select(mvcResult).size(), is(expectedCount));
    }

    /**
     * HTML 内の cssQuery で指定された要素が存在するかチェックする
     *
     * @return {@link ResultMatcher}
     */
    public ResultMatcher exists() {
        return mvcResult -> assertTrue("cssQuery '" + this.cssQuery + "' does not exist"
                , select(mvcResult).size() != 0);
    }

    /**
     * HTML 内の cssQuery で指定された要素が存在しないかチェックする
     *
     * @return {@link ResultMatcher}
     */
    public ResultMatcher notExists() {
        return mvcResult -> assertTrue("cssQuery '" + this.cssQuery + "' exists"
                , select(mvcResult).size() == 0);
    }

    private Document parseHtml(MvcResult mvcResult) throws UnsupportedEncodingException {
        return Jsoup.parse(mvcResult.getResponse().getContentAsString());
    }

    private Elements select(MvcResult mvcResult) throws UnsupportedEncodingException {
        return parseHtml(mvcResult).select(this.cssQuery);
    }

    private Element selectFirst(MvcResult mvcResult) throws UnsupportedEncodingException {
        Element element = select(mvcResult).first();
        if (element == null) {
            throw new AssertionError(
                    String.format("指定された cssQuery の Element は存在しません ( cssQuery = %s )", this.cssQuery));
        }
        return element;
    }

}

HtmlResultMatchersTest.groovy

package ksbysample.common.test.matcher

import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.test.web.servlet.DefaultMvcResult
import spock.lang.Specification
import spock.lang.Unroll

class HtmlResultMatchersTest extends Specification {

    def TEST_HTML = '''\
        <html>
            <body>
                <div id="title">メニュー一覧</div>
                <div>
                    <ul class="menu">
                        <li class="item">メニュー1</li>
                        <li class="item">メニュー2</li>
                    </ul>
                </div>
            </body>
        </html>
    '''

    def mvcResult

    def setup() {
        def mockRequest = new MockHttpServletRequest()
        def mockResponse = Mock(MockHttpServletResponse)
        mockResponse.getContentAsString() >> TEST_HTML
        this.mvcResult = new DefaultMvcResult(mockRequest, mockResponse)
    }

    @Unroll
    def "html(#cssQuery).text(#text)_要素がありテキストが一致する場合"() {
        expect:
        def htmlResultMatchers = HtmlResultMatchers.html(cssQuery)
        def resultMatcher = htmlResultMatchers.text(text)
        resultMatcher.match(this.mvcResult)

        where:
        cssQuery                || text
        "#title"                || "メニュー一覧"
        "ul.menu li.item:eq(0)" || "メニュー1"
    }

    def "html().text()_要素はあるがテキストが一致しない場合 AssertionError が throw される"() {
        when:
        def htmlResultMatchers = HtmlResultMatchers.html("#title")
        def resultMatcher = htmlResultMatchers.text("メニュー")
        resultMatcher.match(this.mvcResult)

        then:
        AssertionError e = thrown()
        e.getMessage() contains ""
    }

    def "html().text()_要素がない場合 AssertionError が throw される"() {
        when:
        def htmlResultMatchers = HtmlResultMatchers.html("#titlex")
        def resultMatcher = htmlResultMatchers.text("メニュー一覧")
        resultMatcher.match(this.mvcResult)

        then:
        AssertionError e = thrown()
        e.getMessage() contains "指定された cssQuery の Element は存在しません ( cssQuery = #titlex )"
    }

    @Unroll
    def "html(#cssQuery).count(#count)"() {
        expect:
        def htmlResultMatchers = HtmlResultMatchers.html(cssQuery)
        def resultMatcher = htmlResultMatchers.count(count)
        resultMatcher.match(this.mvcResult)

        where:
        cssQuery     || count
        "#title"     || 1
        "#titlex"    || 0
        "ul.menu li" || 2
    }

    def "html().exists()_要素がある場合 AssertionError は throw されない"() {
        when:
        def htmlResultMatchers = HtmlResultMatchers.html("#title")
        def resultMatcher = htmlResultMatchers.exists()
        resultMatcher.match(this.mvcResult)

        then:
        notThrown(AssertionError)
    }

    def "html().exists()_要素がない場合 AssertionError が throw される"() {
        when:
        def htmlResultMatchers = HtmlResultMatchers.html("#titlex")
        def resultMatcher = htmlResultMatchers.exists()
        resultMatcher.match(this.mvcResult)

        then:
        AssertionError e = thrown()
        e.getMessage() contains "cssQuery '#titlex' does not exist"
    }

    def "html().notExists()_要素がない場合 AssertionError は throw されない"() {
        when:
        def htmlResultMatchers = HtmlResultMatchers.html("#titlex")
        def resultMatcher = htmlResultMatchers.notExists()
        resultMatcher.match(this.mvcResult)

        then:
        notThrown(AssertionError)
    }

    def "html().notExists()_要素がある場合 AssertionError は throw される"() {
        when:
        def htmlResultMatchers = HtmlResultMatchers.html("#title")
        def resultMatcher = htmlResultMatchers.notExists()
        resultMatcher.match(this.mvcResult)

        then:
        AssertionError e = thrown()
        e.getMessage() contains "cssQuery '#title' exists"
    }

}

AdminLibraryControllerTest.java

        @Test
        public void 管理権限を持つユーザは検索対象図書館登録画面を表示できる_図書館未選択時() throws Exception {
            mvc.authTanakaTaro.perform(get("/admin/library"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(html("p.navbar-text.noselected-library").text("※図書館が選択されていません"));
        }

        @Test
        @TestData("web/admin/library/testdata/001")
        public void 管理権限を持つユーザは検索対象図書館登録画面を表示できる_図書館選択時() throws Exception {
            mvc.authTanakaTaro.perform(get("/admin/library"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(html("p.navbar-text.selected-library").text("選択中:図書館サンプル"));
        }
  • 管理権限を持つユーザは検索対象図書館登録画面を表示できる_図書館未選択時() テストメソッドの .andExpect(xpath("//p[@class='navbar-text noselected-library']").string("※図書館が選択されていません"));.andExpect(html("p.navbar-text.noselected-library").text("※図書館が選択されていません")); に変更します。
  • 管理権限を持つユーザは検索対象図書館登録画面を表示できる_図書館選択時() テストメソッドの .andExpect(xpath("//p[@class='navbar-text selected-library']").string("選択中:図書館サンプル"));.andExpect(html("p.navbar-text.selected-library").text("選択中:図書館サンプル")); に変更します。

履歴

2017/05/23
初版発行。

IntelliJ IDEA を 2017.1.2 → 2017.1.3 へ、Git for Windows を 2.12.2(2) → 2.13.0 へバージョンアップ

IntelliJ IDEA を 2017.1.2 → 2017.1.3 へバージョンアップする

IntelliJ IDEA の 2017.1.3 がリリースされたのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates…」を選択します。

  2. 「Platform and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170520170551p:plain

  3. Plugin の update も表示されたので、チェックしたまま「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170520170856p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。

  5. メイン画面が表示されます。IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20170520172220p:plain

  6. 処理が終了すると Gradle Tool Window のツリーの表示が other グループしかない初期の状態に戻っていますので、左上の「Refresh all Gradle projects」ボタンをクリックして更新します。更新が完了すると build グループ等が表示されます。

  7. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2017.1.3 へバージョンアップされていることを確認します。

  8. clean タスク実行 → Rebuild Project 実行すると “Can’t force a new processing round. Lombok won’t work.” という Warning が表示されました。

    f:id:ksby:20170520173351p:plain

    今回 Error-prone の Plugin を Update したので、おそらくその影響でしょう。Error-prone の Plugin を1つ前のバージョンに戻すことにします。

    https://plugins.jetbrains.com/plugin/7349-error-prone-compiler-integration にアクセスし VERSION 162.1120 のファイル error-prone-162.1120.zip をダウンロードします。

    ダウンロード後 IntelliJ IDEA のメインメニューから「File」-「Settings…」を選択して「Settings」ダイアログを開き、「Plugins」画面の「Install plugin from disk…」ボタンを押してインストールします。

    f:id:ksby:20170520191114p:plain

  9. 再び clean タスク → Rebuild Project を実行すると今度は正常に終了しました。build タスクを実行して “BUILD SUCCESSFUL” のメッセージが表示されることも確認できました。

    f:id:ksby:20170520192142p:plain

  10. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’ with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20170520192500p:plain

Git for Windows を 2.12.2(2) → 2.13.0 へバージョンアップする

Git for Windows の 2.13.0 がリリースされていたのでバージョンアップします。

  1. https://git-for-windows.github.io/ の「Download」ボタンをクリックして Git-2.13.0-64-bit.exe をダウンロードします。

  2. Git-2.13.0-64-bit.exe を実行します。

  3. 「Git 2.13.0 Setup」ダイアログが表示されます。[Next >]ボタンをクリックします。

  4. 「Select Components」画面が表示されます。「Git LFS(Large File Support)」だけチェックした状態で [Next >]ボタンをクリックします。

  5. 「Adjusting your PATH environment」画面が表示されます。中央の「Use Git from the Windows Command Prompt」が選択されていることを確認後、[Next >]ボタンをクリックします。

  6. 「Choosing HTTPS transport backend」画面が表示されます。「Use the OpenSSL library」が選択されていることを確認後、[Next >]ボタンをクリックします。

  7. 「Configuring the line ending conversions」画面が表示されます。「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  8. 「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  9. 「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Install]ボタンをクリックします。

  10. インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View Release Notes」のチェックを外した後、「Finish」ボタンをクリックしてインストーラーを終了します。

  11. コマンドプロンプトを起動して git のバージョンが git version 2.13.0.windows.1 になっていることを確認します。

    f:id:ksby:20170521034252p:plain

  12. git-cmd.exe を起動して日本語の表示・入力が問題ないかを確認します。

    f:id:ksby:20170521034450p:plain

  13. 特に問題はないようですので、2.13.0 で作業を進めたいと思います。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その4 )( 1.4系 → 1.5系で変更された点を修正する )

概要

記事一覧はこちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その3 )( Run ‘All Tests’ with Coverage 実行時に出るエラーを解消する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Release Notes を見て 1.4系 → 1.5系で変更された点を修正します。

参照したサイト・書籍

  1. Spring Boot 1.5 Release Notes
    https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-1.5-Release-Notes

  2. Spring Boot 1.5の主な変更点
    http://qiita.com/kazuki43zoo/items/c26d99bb57888e42eaf1

目次

  1. Devtools excluded by default
  2. 次回は。。。

手順

Devtools excluded by default

spring-boot-devtools がデフォルトで fat jar から除外されるようになりました。

1.4 系の時のソースに切り戻した後、build.gradle の bootRepackage の中の excludeDevtools = trueコメントアウトして jar ファイルを生成してみます。

bootRepackage {
    mainClass = 'ksbysample.webapp.lending.Application'
//    excludeDevtools = true
}

生成された ksbysample-webapp-lending-1.4.6-RELEASE.jar を解凍すると、BOOT-INF\lib の下に spring-boot-devtools-1.4.6.RELEASE.jar が入っています。

f:id:ksby:20170520150347p:plain

excludeDevtools = trueコメントアウトを元に戻すと、spring-boot-devtools-1.4.6.RELEASE.jar は入りません。

f:id:ksby:20170520151118p:plain

feature/130-issue ブランチに戻して build.gradle から excludeDevtools = true を削除して jar ファイルを生成してみます。

bootRepackage {
    mainClass = 'ksbysample.webapp.lending.Application'
}

生成された ksbysample-webapp-lending-1.5.3-RELEASE.jar を解凍すると spring-boot-devtools-1.5.3.RELEASE.jar は入っていませんでした。

f:id:ksby:20170520152522p:plain

次回は。。。

1.5 では特にソースの変更が必要なところはありませんでした。次回は Thymeleaf を 3系へバージョンアップします。

ソースコード

履歴

2017/05/20
初版発行。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その3 )( Run 'All Tests' with Coverage 実行時に出るエラーを解消する )

概要

記事一覧はこちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その1 )( 概要 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Run ‘All Tests’ with Coverage 実行時に出るエラーの解消

参照したサイト・書籍

目次

  1. java.lang.IllegalStateException: Failed to load ApplicationContext
  2. 次回は。。。

手順

java.lang.IllegalStateException: Failed to load ApplicationContext

java.lang.IllegalStateException: Failed to load ApplicationContext の下に出力されるメッセージの内、Caused by: から始まるエラーメッセージは以下のものでした。

  • Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'webSecurityConfig': Unsatisfied dependency expressed through method 'setContentNegotationStrategy' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$EnableWebMvcConfiguration': Invocation of init method failed; nested exception is java.lang.IllegalStateException: Found unexpected validator configuration. A Spring Boot MVC validator should be registered as bean named 'mvcValidator' and not returned from WebMvcConfigurer.getValidator()
  • Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$EnableWebMvcConfiguration': Invocation of init method failed; nested exception is java.lang.IllegalStateException: Found unexpected validator configuration. A Spring Boot MVC validator should be registered as bean named 'mvcValidator' and not returned from WebMvcConfigurer.getValidator()
  • Caused by: java.lang.IllegalStateException: Found unexpected validator configuration. A Spring Boot MVC validator should be registered as bean named 'mvcValidator' and not returned from WebMvcConfigurer.getValidator()

Spring Boot MVC validator は mvcValidator という名前の Bean で登録されているべきなのに、WebMvcConfigurer.getValidator() が返さない、ということのようです。

例外に出力されているソースを追ってみると、org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.EnableWebMvcConfiguration#afterPropertiesSet でこのメッセージを出力していることが確認できます。getValidator() == null でないと Assert に引っかかります。

f:id:ksby:20170516055744p:plain

getValidator() は org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration#getValidator のことで、以下のコードです。

f:id:ksby:20170516060229p:plain

configurers.getValidator() は org.springframework.web.servlet.config.annotation.WebMvcConfigurerComposite#getValidator のことで、以下のコードです。このメソッドが null を返さないのでエラーになるようですね。

f:id:ksby:20170516060625p:plain

デバッガで確認してみると、確かに null ではなく LocalValidatorFactoryBean を返しています。

f:id:ksby:20170516061605p:plain

f:id:ksby:20170516061206p:plain

自分で実装したコードを見ると、確かに Validator インターフェースの Bean は mvcValidator ではなく validator という名前にしています。

f:id:ksby:20170516062010p:plain

そして validator Bean を ksbysample.webapp.lending.config.WebMvcConfig#getValidator で返しています。

f:id:ksby:20170516062154p:plain

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その20 )( 気になった点を修正する ) の記事で ValidationMessages_ja_JP.properties をやめて messages.properties に1本化するために書いたものでした。

ここまでの調査から対応方法を考えると、以下のようにすれば良い気がします。

  • ksbysample.webapp.lending.config.WebMvcConfig#getValidator は null を返さないといけないようなので、ksbysample.webapp.lending.config.WebMvcConfig 自体は削除する。
  • validator Bean ではなく mvcValidator Bean にする。

“mvcValidator” で検索してみると org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.EnableWebMvcConfiguration#mvcValidator に mvcValidator Bean を取得して mvcValidator Bean を生成する?、というよく分からないコードがありました。

f:id:ksby:20170516063841p:plain

Spring Boot 1.4.6 を使用している時のコードに戻してみると、org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.EnableWebMvcConfiguration#mvcValidator は 1.5.3 と同じでした。

f:id:ksby:20170516070741p:plain

org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.EnableWebMvcConfiguration#afterPropertiesSet は存在しませんでした。

おそらく 1.4 の頃から mvcValidator という名前の Bean として登録しておけばよいだけで、WebMvcConfigurerAdapter の継承クラスで getValidator メソッドを Override する必要はなかった、のではないかと思います。

上に書いた2点の対応方法で良さそうなので、コードを修正します。

まず src/main/java/ksbysample/webapp/lending/config/WebMvcConfig.java を削除します。

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

最後に以下のクラスのフィールド private final Validator validator;private final Validator mvcValidator; に変更します。

  • src/main/java/ksbysample/webapp/lending/web/springmvcmemo/BeanValidationGroupController.java
  • src/test/java/ksbysample/webapp/lending/values/validation/ValuesEnumValidatorTest.java

コードを修正したら clean タスク → Rebuild Project → build タスクを実行してみます。今度は “BUILD SUCCESSFUL” が表示されて成功しました。

f:id:ksby:20170517002651p:plain

Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」も実行してみます。こちらも無事全てのテストが成功しました。

f:id:ksby:20170517003210p:plain

Hibernate Validator を使用しているところで messages.properties に記述した日本語のメッセージが使用されるか確認します。

bootRun で Tomcat を起動した後、Hibernate Validator の @NotBlank アノテーションで入力チェックをしている http://localhost:8080/springMvcMemo/beanValidationGroup にアクセスします。

f:id:ksby:20170520143025p:plain

「データ更新」ボタンをクリックすると、日本語のメッセージ「必須の入力項目です。」が表示されました。

f:id:ksby:20170520143232p:plain

mvcValidator Bean をコメントアウトしてから Tomcat を再起動して同じ操作をしてみると、

f:id:ksby:20170520143455p:plain

今度は英語のメッセージが表示されました。mvcValidator Bean を定義するだけで messages.properties の日本語メッセージが使用されるようになるようです。

f:id:ksby:20170520143722p:plain

次回は。。。

1.5 系ではこう書くべきという点があるか確認し、変更した方がよいところを修正します。

ソースコード

ApplicationConfig.java

    @Bean
    public Validator mvcValidator() {
        LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
        localValidatorFactoryBean.setValidationMessageSource(this.messageSource);
        return localValidatorFactoryBean;
    }
  • メソッド名を validatormvcValidator に変更します。

履歴

2017/05/17
初版発行。
2017/05/20
* http://localhost:8080/springMvcMemo/beanValidationGroup での動作確認を追加しました。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その2 )( build.gradle の修正 )

概要

記事一覧はこちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その1 )( 概要 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • build.gradle の修正

参照したサイト・書籍

目次

  1. 1.5.x ブランチの作成
  2. Spring Initializr で 1.5.3 のプロジェクトを作成する
  3. build.gradle を修正して build してみる
  4. 次回は。。。

手順

1.5.x ブランチの作成

  1. master から 1.5.x ブランチを、1.5.x から feature/130-issue ブランチを作成します。

Spring Initializr で 1.5.3 のプロジェクトを作成する

  1. IntelliJ IDEA のメインメニューから「File」-「Close Project」を選択して「Welcome to IntelliJ IDEA」ダイアログに戻ります。

  2. 「Welcome to IntelliJ IDEA」ダイアログで「Create New Project」をクリックします。

    f:id:ksby:20170513204210p:plain

  3. 「New Project」ダイアログが表示されます。画面左側のリストから「Spring Initializr」を選択した後、「Next」ボタンをクリックします。

    f:id:ksby:20170513204405p:plain

  4. 次の画面が表示されます。「Type」で「Gradle Project」を選択した後、「Next」ボタンをクリックします。

    f:id:ksby:20170513204637p:plain

  5. 次の画面が表示されます。画面中央上の「Spring Boot」で「1.5.3」を選択してから ksbysample-webapp-lending プロジェクトで使用している以下の項目をチェックした後、「Next」ボタンをクリックします。

    f:id:ksby:20170513204953p:plain f:id:ksby:20170513205100p:plain f:id:ksby:20170513205253p:plain f:id:ksby:20170513205434p:plain f:id:ksby:20170513205553p:plain

  6. 次の画面が表示されます。「Project location」を “C:\project-springboot\demo” に変更した後、「Finish」ボタンをクリックします。

    f:id:ksby:20170513210129p:plain

  7. 「Import Module from Gradle」ダイアログが表示されます。「Create directories for empty content roots automatically」をチェックした後、「OK」ボタンをクリックします。

    f:id:ksby:20170513210308p:plain

これでプロジェクトが作成されて以下の build.gradle が作成されました。

buildscript {
    ext {
        springBootVersion = '1.5.3.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-redis')
    compile('org.springframework.boot:spring-boot-starter-freemarker')
    compile('org.springframework.boot:spring-boot-starter-mail')
    compile('org.springframework.boot:spring-boot-starter-security')
    compile('org.springframework.session:spring-session')
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile('org.springframework.boot:spring-boot-starter-web')
    runtime('org.springframework.boot:spring-boot-devtools')
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

1.4.4 で生成した時と異なる点は特にありませんでした。今回は何も反映しません。

build.gradle を修正して build してみる

  1. build.gradle を リンク先の内容 に変更します。

  2. Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。今回は正常に更新できました。

  3. clean タスク実行 → Rebuild Project 実行 を実行します。こちらも正常に終了しました。

  4. build タスクを実行します。が、大量の java.lang.IllegalStateException が発生してテストが全く終わりませんでした。。。

    f:id:ksby:20170513235010p:plain

  5. Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」も実行してみます。

    こちらも java.lang.IllegalStateException: Failed to load ApplicationContext というエラーメッセージが出てテストは失敗しました。ほとんどのテストが失敗していたので、途中で中断しています。

    f:id:ksby:20170513235559p:plain

次回は。。。

Run ‘All Tests’ with Coverage 実行時に出る java.lang.IllegalStateException: Failed to load ApplicationContext のエラーの解消→ build タスクの再実行(エラーが出れば解消します)の順で進める予定です。

ソースコード

build.gradle

group 'ksbysample'
version '1.5.3-RELEASE'

buildscript {
    ext {
        springBootVersion = '1.5.3.RELEASE'
    }
    repositories {
        jcenter()
        maven { url "http://repo.spring.io/repo/" }
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        // for Error Prone ( http://errorprone.info/ )
        classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.10")
        // for Grgit
        classpath("org.ajoberstar:grgit:1.9.2")
        // Gradle Download Task
        classpath("de.undercouch:gradle-download-task:3.2.0")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'de.undercouch.download'
apply plugin: 'groovy'
apply plugin: 'net.ltgt.errorprone'
apply plugin: 'checkstyle'
apply plugin: 'findbugs'

sourceCompatibility = 1.8
targetCompatibility = 1.8

task wrapper(type: Wrapper) {
    gradleVersion = '2.13'
}

[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing,-path']
compileJava.options.compilerArgs += ['-Xep:RemoveUnusedImports:WARN']

// for Doma 2
// JavaクラスとSQLファイルの出力先ディレクトリを同じにする
processResources.destinationDir = compileJava.destinationDir
// コンパイルより前にSQLファイルを出力先ディレクトリにコピーするために依存関係を逆転する
compileJava.dependsOn processResources

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

configurations {
    // for Doma 2
    domaGenRuntime
}

checkstyle {
    configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml")
    toolVersion = '7.7'
    sourceSets = [project.sourceSets.main]
}

findbugs {
    toolVersion = '3.0.1'
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    effort = "max"
    excludeFilter = file("${rootProject.projectDir}/config/findbugs/findbugs-exclude.xml")
}

tasks.withType(FindBugs) {
    reports {
        xml.enabled = false
        html.enabled = true
    }
}

repositories {
    jcenter()
}

dependencyManagement {
    imports {
        mavenBom("io.spring.platform:platform-bom:Brussels-SR2") {
            bomProperty 'guava.version', '21.0'
        }
    }
}

bootRepackage {
    mainClass = 'ksbysample.webapp.lending.Application'
    excludeDevtools = true
}

dependencies {
    def jdbcDriver = "org.postgresql:postgresql:42.0.0"
    def spockVersion = "1.1-groovy-2.4"
    def domaVersion = "2.16.0"
    def lombokVersion = "1.16.16"
    def errorproneVersion = "2.0.15"

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity4")
    compile("org.thymeleaf.extras:thymeleaf-extras-java8time")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.boot:spring-boot-starter-freemarker")
    compile("org.springframework.boot:spring-boot-starter-mail")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.springframework.boot:spring-boot-starter-data-redis")
    compile("org.springframework.boot:spring-boot-starter-amqp")
    compile("org.springframework.boot:spring-boot-devtools")
    compile("org.springframework.session:spring-session")
    compile("org.springframework.retry:spring-retry")
    compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
    compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
    compile("com.google.guava:guava")
    compile("org.apache.commons:commons-lang3")
    compile("org.codehaus.janino:janino")
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.springframework.security:spring-security-test")
    testCompile("org.yaml:snakeyaml")
    testCompile("org.mockito:mockito-core")

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    runtime("${jdbcDriver}")
    compile("com.integralblue:log4jdbc-spring-boot-starter:1.0.1")
    compile("org.simpleframework:simple-xml:2.7.1")
    compile("com.univocity:univocity-parsers:2.4.1")
    testCompile("org.dbunit:dbunit:2.5.3")
    testCompile("com.icegreen:greenmail:1.5.4")
    testCompile("org.assertj:assertj-core:3.7.0")
    testCompile("com.jayway.jsonpath:json-path:2.2.0")
    testCompile("org.spockframework:spock-core:${spockVersion}") {
        exclude module: "groovy-all"
    }
    testCompile("org.spockframework:spock-spring:${spockVersion}") {
        exclude module: "groovy-all"
    }
    testCompile("com.google.code.findbugs:jsr305:3.0.2")

    // for lombok
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
    testCompileOnly("org.projectlombok:lombok:${lombokVersion}")

    // for Doma
    compile("org.seasar.doma:doma:${domaVersion}")
    domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}")
    domaGenRuntime("${jdbcDriver}")

    // for Error Prone ( http://errorprone.info/ )
    errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}")
    compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}")
}

bootRun {
    jvmArgs = ['-Dspring.profiles.active=develop']
}

test {
    jvmArgs = ['-Dspring.profiles.active=unittest']
}

// for Doma-Gen
task domaGen << {
    // まず変更が必要なもの
    def rootPackageName  = 'ksbysample.webapp.lending'
    def daoPackagePath   = 'src/main/java/ksbysample/webapp/lending/dao'
    def dbUrl            = 'jdbc:postgresql://localhost/ksbylending'
    def dbUser           = 'ksbylending_user'
    def dbPassword       = 'xxxxxxxx'
    def tableNamePattern = '.*'
    // おそらく変更不要なもの
    def importOfComponentAndAutowiredDomaConfig = "${rootPackageName}.util.doma.ComponentAndAutowiredDomaConfig"
    def workDirPath      = 'work'
    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()
    }

    // 生成された Dao インターフェースを作業用ディレクトリにコピーし、
    // @ComponentAndAutowiredDomaConfig アノテーションを付加する
    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')
        }
    }

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

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

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

    // 自動生成したファイルを git add する
    addGit()
}

task downloadCssFontsJs << {
    def staticDirPath   = 'src/main/resources/static'
    def workDirPath     = 'work'
    def adminLTEVersion     = '2.2.0'
    def jQueryVersion       = '2.1.4'
    def fontAwesomeVersion  = '4.3.0'
    def ioniconsVersion     = '2.0.1'
    def html5shivJsVersion  = '3.7.2'
    def respondMinJsVersion = '1.4.2'

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

    // Bootstrap & AdminLTE Dashboard & Control Panel Template
    downloadAdminLTE("${adminLTEVersion}", "${jQueryVersion}", "${workDirPath}", "${staticDirPath}")

    // Font Awesome Icons
    downloadFontAwesome("${fontAwesomeVersion}", "${workDirPath}", "${staticDirPath}")

    // Ionicons
    downloadIonicons("${ioniconsVersion}", "${workDirPath}", "${staticDirPath}")

    // html5shiv.js
    downloadHtml5shivJs("${html5shivJsVersion}", "${workDirPath}", "${staticDirPath}")

    // respond.min.js
    downloadRespondMinJs("${respondMinJsVersion}", "${workDirPath}", "${staticDirPath}")

    // fileinput.min.js ( v4.2.7 )
    downloadBootstrapFileInputMinJs("${workDirPath}", "${staticDirPath}")

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

    // 追加したファイルを git add する
    addGit()
}

task printClassWhatNotMakeTest << {
    def srcDir = new File("src/main/java")
    def excludePaths = [
            "src/main/java/ksbysample/webapp/lending/Application.java"
            , "src/main/java/ksbysample/webapp/lending/config"
            , "src/main/java/ksbysample/webapp/lending/cookie"
            , "src/main/java/ksbysample/webapp/lending/dao"
            , "src/main/java/ksbysample/webapp/lending/entity"
            , "src/main/java/ksbysample/webapp/lending/exception"
            , "src/main/java/ksbysample/webapp/lending/helper/download/booklistcsv"
            , "src/main/java/ksbysample/webapp/lending/helper/download/DataDownloadHelper.java"
            , "src/main/java/ksbysample/webapp/lending/helper/page/PagenationHelper.java"
            , "src/main/java/ksbysample/webapp/lending/security/LendingUser.java"
            , "src/main/java/ksbysample/webapp/lending/security/RoleAwareAuthenticationSuccessHandler.java"
            , "src/main/java/ksbysample/webapp/lending/service/calilapi/response"
            , "src/main/java/ksbysample/webapp/lending/service/file/BooklistCSVRecord.java"
            , "src/main/java/ksbysample/webapp/lending/service/openweathermapapi"
            , "src/main/java/ksbysample/webapp/lending/service/queue/InquiringStatusOfBookQueueMessage.java"
            , "src/main/java/ksbysample/webapp/lending/util/doma"
            , "src/main/java/ksbysample/webapp/lending/util/velocity/VelocityUtils.java"
            , "src/main/java/ksbysample/webapp/lending/values/validation/ValuesEnum.java"
            , "src/main/java/ksbysample/webapp/lending/view/BookListCsvView.java"
            , "src/main/java/ksbysample/webapp/lending/web/.+/.+Service.java"
            , "src/main/java/ksbysample/webapp/lending/webapi/common/CommonWebApiResponse.java"
            , "src/main/java/ksbysample/webapp/lending/webapi/weather"
    ]
    def excludeFileNamePatterns = [
            ".*EventListener.java"
            , ".*Dto.java"
            , ".*Form.java"
            , ".*Values.java"
    ]

    compareSrcAndTestDir(srcDir, excludePaths, excludeFileNamePatterns)
}

/* -----------------------------------------------------------------------------
 * メソッド定義部
 ---------------------------------------------------------------------------- */
void clearDir(String dirPath) {
    delete dirPath
}

void addGit() {
    def grgit = org.ajoberstar.grgit.Grgit.open(dir: project.projectDir)
    grgit.add(patterns: ['.'])
}

void downloadAdminLTE(String adminLTEVersion, String jQueryVersion, String workDirPath, String staticDirPath) {
    download {
        src "https://codeload.github.com/almasaeed2010/AdminLTE/zip/v${adminLTEVersion}"
        dest new File("${workDirPath}/download/AdminLTE-${adminLTEVersion}.zip")
    }
    copy {
        from zipTree("${workDirPath}/download/AdminLTE-${adminLTEVersion}.zip")
        into "${workDirPath}/unzip"
    }
    copy {
        from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/bootstrap/css"
        into "${staticDirPath}/css"
    }
    copy {
        from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/bootstrap/fonts"
        into "${staticDirPath}/fonts"
    }
    copy {
        from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/bootstrap/js"
        into "${staticDirPath}/js"
    }
    copy {
        from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/dist/css"
        into "${staticDirPath}/css"
    }
    copy {
        from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/dist/js"
        into "${staticDirPath}/js"
    }
    copy {
        from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/plugins/jQuery/jQuery-${jQueryVersion}.min.js"
        into "${staticDirPath}/js"
    }
    delete "${staticDirPath}/js/pages"
    delete "${staticDirPath}/js/demo.js"
}

void downloadFontAwesome(String fontAwesomeVersion, String workDirPath, String staticDirPath) {
    download {
        src "http://fortawesome.github.io/Font-Awesome/assets/font-awesome-${fontAwesomeVersion}.zip"
        dest new File("${workDirPath}/download/font-awesome-${fontAwesomeVersion}.zip")
    }
    copy {
        from zipTree("${workDirPath}/download/font-awesome-${fontAwesomeVersion}.zip")
        into "${workDirPath}/unzip"
    }
    copy {
        from "${workDirPath}/unzip/font-awesome-${fontAwesomeVersion}/css/font-awesome.min.css"
        into "${staticDirPath}/css"
    }
    copy {
        from "${workDirPath}/unzip/font-awesome-${fontAwesomeVersion}/fonts"
        into "${staticDirPath}/fonts"
    }
}

void downloadIonicons(String ioniconsVersion, String workDirPath, String staticDirPath) {
    download {
        src "https://codeload.github.com/driftyco/ionicons/zip/v${ioniconsVersion}"
        dest new File("${workDirPath}/download/ionicons-${ioniconsVersion}.zip")
    }
    copy {
        from zipTree("${workDirPath}/download/ionicons-${ioniconsVersion}.zip")
        into "${workDirPath}/unzip"
    }
    copy {
        from "${workDirPath}/unzip/ionicons-${ioniconsVersion}/css/ionicons.min.css"
        into "${staticDirPath}/css"
    }
    copy {
        from "${workDirPath}/unzip/ionicons-${ioniconsVersion}/fonts"
        into "${staticDirPath}/fonts"
    }
}

void downloadHtml5shivJs(String html5shivJsVersion, String workDirPath, String staticDirPath) {
    download {
        src "https://oss.maxcdn.com/html5shiv/${html5shivJsVersion}/html5shiv.min.js"
        dest new File("${workDirPath}/download/html5shiv.min.js")
    }
    copy {
        from "${workDirPath}/download/html5shiv.min.js"
        into "${staticDirPath}/js"
    }
}

void downloadRespondMinJs(String respondMinJsVersion, String workDirPath, String staticDirPath) {
    download {
        src "https://oss.maxcdn.com/respond/${respondMinJsVersion}/respond.min.js"
        dest new File("${workDirPath}/download/respond.min.js")
    }
    copy {
        from "${workDirPath}/download/respond.min.js"
        into "${staticDirPath}/js"
    }
}

void downloadBootstrapFileInputMinJs(String workDirPath, String staticDirPath) {
    download {
        src "https://github.com/kartik-v/bootstrap-fileinput/zipball/master"
        dest new File("${workDirPath}/download/kartik-v-bootstrap-fileinput.zip")
    }
    copy {
        from zipTree("${workDirPath}/download/kartik-v-bootstrap-fileinput.zip")
        into "${workDirPath}/unzip"
    }
    copy {
        from "${workDirPath}/unzip/kartik-v-bootstrap-fileinput-883d8b6/js/fileinput.min.js"
        into "${staticDirPath}/js"
    }
    copy {
        from "${workDirPath}/unzip/kartik-v-bootstrap-fileinput-883d8b6/js/fileinput_locale_ja.js"
        into "${staticDirPath}/js"
    }
    copy {
        from "${workDirPath}/unzip/kartik-v-bootstrap-fileinput-883d8b6/css/fileinput.min.css"
        into "${staticDirPath}/css"
    }
}

def compareSrcAndTestDir(srcDir, excludePaths, excludeFileNamePatterns) {
    def existFlg
    def testDirAndTestClassNameList = [
            ["src/test/java", "Test.java"]
            , ["src/test/groovy", "Test.groovy"]
    ]

    for (srcFile in srcDir.listFiles()) {
        String srcFilePath = (srcFile.toPath() as String).replaceAll("\\\\", "/")
        existFlg = false

        for (exclude in excludePaths) {
            if (srcFilePath =~ /^${exclude as String}/) {
                existFlg = true
                break
            }
        }
        if (existFlg == true) continue

        for (exclude in excludeFileNamePatterns) {
            if (srcFilePath =~ /${exclude as String}/) {
                existFlg = true
                break
            }
        }
        if (existFlg == true) continue

        if (srcFile.isDirectory()) {
            compareSrcAndTestDir(srcFile, excludePaths, excludeFileNamePatterns)
        } else {
            def nextFlg = false
            for (testDirAndTestClassName in testDirAndTestClassNameList) {
                def testDir = testDirAndTestClassName[0]
                def testClassName = testDirAndTestClassName[1]

                String testFilePath = srcFilePath.replaceFirst(/^src\/main\/java/, testDir)
                        .replaceFirst(/\.java$/, testClassName)
                def testFile = new File(testFilePath)
                if (testFile.exists()) {
                    nextFlg = true
                    break
                }
            }
            if (nextFlg) continue

            println(srcFilePath)
        }
    }
}
  • Spring Boot のバージョンアップ対応として以下の点を変更します。
    • version '1.4.6-RELEASE'version '1.5.3-RELEASE' に変更します。
    • buildscript の以下の点を変更します。
      • springBootVersion = '1.4.6.RELEASE'springBootVersion = '1.5.3.RELEASE' に変更します。
    • dependencyManagement の以下の点を変更します。
      • mavenBom("io.spring.platform:platform-bom:Athens-SR5")mavenBom("io.spring.platform:platform-bom:Athens-SR5") に変更します。
  • ライブラリを最新バージョンにアップデートするために以下の点を変更します。
    • def spockVersion = "1.1-groovy-2.4-rc-4"def spockVersion = "1.1-groovy-2.4" に変更します。
    • testCompile("org.assertj:assertj-core:3.6.2")testCompile("org.assertj:assertj-core:3.7.0") に変更します。

履歴

2017/05/14
初版発行。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その1 )( 概要 )

概要

記事一覧はこちらです。

  • 「Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る」で作成した Web アプリケーション ( ksbysample-webapp-lending ) の Spring Boot のバージョンを 1.4.6 → 1.5.3 へバージョンアップします。
  • 進め方は以下の方針とします。
    • Git のブランチは 1.5.x を作成して、そちらで作業します。Spring Boot のバージョンと合わせます。
    • 最初に build.gradle を修正します。
      • Spring Boot のバージョン番号を 1.5.3 に、Spring IO Platform の BOM を Brussels-SR2 にします。
      • Spring Initializr で 1.5.3 のプロジェクトを作成して、修正した方がよさそうな点があれば反映します。
      • ライブラリは最新バージョンにアップデートします。
    • プロジェクトを build し直してエラーが出る点があれば修正し、まずはここまでで動くようにします。
    • その後で 1.5 系ではこう書くべきという点があるか確認し、変更した方がよいところを修正します。

 
1.5 のリリースノートはこちらです。

Spring Boot 1.5 Release Notes
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-1.5-Release-Notes

履歴

2017/05/13
初版発行。