Vue.js/WebApi版

1. 概要

Vue.js/WebApiを利用して実装されたサンプルアプリケーションです。
Vue.js部分の説明を公式サイトを参照してください。
Vue.js以外に、vue-routervue-i18naxios のライブラリを利用しています。
WebApi部分のカスタマイズする方法は開発者ガイドのWebApiの章を参照してください。

2. セットアップ

サンプルアプリケーションのセットアップ説明です。
もし、既にiPLASSの開発環境の構築チュートリアルを実行できた方で、サンプルアプリケーションのサンプルを動かしたい場合、以下の手順に従って実施してみてください。
それ以外の方は開発環境の構築を先に実施することをお勧めします。

  • サンプルアプリケーションのプロジェクトをGitHubから取得してプロジェクトを作成します。プロジェクトの作成手順はプロジェクトの作成を参照してください。

    サンプルコードで Java8 より上のバージョンのスタイルでコーディングする場合は build.gradle の javaVersion を使用したい Java のバージョンに書き換えてください。
    書き換えた後はGradleプロジェクトのリフレッシュを行い、エラーが出ないことを確認してください。
    Vue Component のビルドに利用する nodejs のバージョンは、nodejsVersion で指定します。

    build.gradle
    plugins {
        id 'com.github.node-gradle.node' version '7.0.1' apply false
    }
    
    apply plugin: 'java'
    apply plugin: 'war'
    apply plugin: 'eclipse-wtp'
    apply plugin: 'com.github.node-gradle.node'
    
    ext {
        javaVersion = JavaVersion.VERSION_1_8 (1)
        nodejsVersion = '16.19.0' (2)
    }
    
    ----------------------------------------以下略----------------------------------------
    1 必要に応じて使用したい Java のバージョンに書き換えます。
    2 ビルドに利用する nodejs のバージョンを指定します。
  • サンプルアプリを起動する前に、Vue Component をビルドする必要があります。
    ビルドを行う前に、webpack.config.js内のstaticContentPathの値を作成したプロジェクト名に修正します。

    var path = require('path')
    var webpack = require('webpack')
    var staticContentPath = '/mtp' (1)
    --------------------------------
    1 この値を '/{作成したプロジェクト名}' に修正する。

    webpack.config.jsの修正後、 Vue Component のビルドをプロジェクトルートから以下のコマンドを用いて実行します。

    gradlew buildVue
  • Vue.js版のサンプルアプリの起動手順はJava/JSP版と同じになっているので、Java/JSP版の起動手順を参照してください。

    このサンプルアプリは開発環境の構築の章で作成したテナントで動かすことを想定しています。
    新規テナントでサンプルを動かしたい場合、Java/JSP版のテナント作成の章を参照してください。

3. 機能

3.1. Top画面の作成

Top画面の構成は下図のようになっています。画面共通で利用するレイアウト用Vueコンポーネント defaultLayout.vueshippingLayout.vue を用意し、Vue-routerを利用することでアクセスされたパスによって実際のコンテンツを切り替えています。

sample ec vuejs webapi template layout

  • Vue.jsの部分

    ファイル名

    /src/main/vue/components/layout/DefaultLayout.vue

    ----------------------------------------以上略----------------------------------------
        <div class="row layout-container">
            <div class="col-md-3 d-none d-md-block">
                <div class="row">
                    <div class="col-12">
                        <div class="list-group list-group-item-dark list-group-flush">
                            <router-link v-bind:to="{name: 'top'}" class="list-group-item list-group-item-action font-weight-bold border-top">{{$t("samples.ec01.layout.defaultLayout.home")}}</router-link>
                            <template v-for="category in categoryList">
                                <router-link v-bind:to="{name: 'category', query: {categoryId: category.oid}}" v-bind:key="category.oid" class="list-group-item list-group-item-action">{{category.name}}</router-link>
                            </template>
                        </div>
                    </div>
                </div>
            </div>
            <router-view></router-view> (1)
        </div>
    ----------------------------------------以下略----------------------------------------
    1 実際のコンテンツを表示する箇所を指定するrouter-viewタグです。
    ※ Vue-Router部分の説明は割愛です。公式サイトを参照してください。

3.2. Bean/Bean Validation

iPLAssのBean/Bean Validation機能を利用する場合、チェックしたいBeanクラスのプロパティにアノテーションをつけることで、バリデーションが自動に実行されます。それに、バリデーション結果を画面に表示できます。

サンプルアプリのお問い合わせ登録画面で入力された値に対してバリデーションが実行される機能を例として説明していきます。

  • 必要なJavaクラスを作成しています。
    プロジェクトのフォルダ src/main/java/samples/ec01/beanを開きます。

    src.main.java
        ┗ samples.ec01
            ┣ bean
            ┗ annotation (1)
                ┃   ┣ Kana.java
                ┃   ┗ ......         
                ┣ ui
                ┣ validator (2)
                ┃   ┣ group (3)
                ┃   ┃   ┗ JapaneseChecks.java
                ┃   ┣ KanaValidator.java
                ┃   ┗ ......
                ┣━ UserBean.java (4)
                ┗ ......
    1 カスタムのバリデーション
    カスタムのバリデーションを samples.ec01.bean.annotation.Kana に記述して、それがカスタムのバリデーターを利用しています。
    2 カスタムのバリデーター
    カスタムのバリデーターを samples.ec01.bean.validator.KanaValidator に記述して、それがカスタムのバリデーションで利用されています。
    3 カスタムのバリデーショングループ
    カスタムのバリデーショングループを samples.ec01.bean.validator.group.JapaneseChecks に記述して、それがBeanクラスのアノテーションで利用されています。
    4 JavaBeanクラス
    samples.ec01.bean.UserBean でカスタムのバリデーションとバリデーショングループを利用しています。
  • バリデーションエラーメッセージを定義する
    Bean Validationエラーメッセージはプロパティファイルに定義し、多言語利用が可能です。

  • コマンドクラスでBean Validationを利用する

    ファイル名

    /src/main/java/samples/ec01/command/inquiry/RegistInquiryCommand.java

    ......
    @WebApi(
            name = "samples/ec01/inquiry/doInquiry",
            displayName = "お問合せ登録WebApi",
            accepts = RequestType.REST_JSON,
            restJson = @RestJson(parameterName = "param"),
            methods = MethodType.POST,
            privileged = true,
            tokenCheck = @WebApiTokenCheck(
                    executeCheck = true,
                    consume = true,
                    exceptionRollback = true),
            results = { RegistInquiryCommand.RESULT_MAPPING_RESULT })
    @CommandClass(
            name = "samples/ec01/inquiry/RegistInquiryCommand",
            displayName = "お問合せ登録コマンド")
    public class RegistInquiryCommand implements Command {
    
        private final BeanParamMapper mapper = new BeanParamMapper().withValidation()
                .whitelistPropertyNameRegex("^(mail|content|familyName(Kana)?|firstName(Kana)?)$"); (1)
        public static final String RESULT_INQUIRY_BEAN = "inquiryBean";
        public static final String RESULT_MAPPING_RESULT = "result";
    
    
        @Override
        public String execute(RequestContext request) {
            // 入力チェック
            InquiryBean inquiryBean = new InquiryBean();
            request.setAttribute(RESULT_INQUIRY_BEAN, inquiryBean);
            try {
                // 日本語専用"name_kana"取得フォーム
                if (Consts.LANGUAGE_JA.equals(TemplateUtil.getLanguage()) || TemplateUtil.getLanguage() == null) {
                    mapper.populate(inquiryBean, request.getParamMap(), Default.class, JapaneseChecks.class); (2)
                } else {
                    mapper.populate(inquiryBean, request.getParamMap(), Default.class); (3)
                }
            } catch (MappingException e) {
                request.setAttribute(RESULT_MAPPING_RESULT, e.getResult()); (4)
                return Constants.CMD_EXEC_ERROR;
            }
    
            Inquiry inquiry = inquiryBean.toEntity();
            // 問い合わせステータス
            // 1 : 未対応
            // 2 : 対応中
            // 3 : 対応完了
            // 4 : 終了
            SelectValue inquiryStatus = new SelectValue(InquiryStatus.NOT_DEAL.getValue());
            inquiry.setInquiryStatus(inquiryStatus);
            // 請求の登録
            EntityDaoHelper.insert(inquiry);
    
            return Constants.CMD_EXEC_SUCCESS;
        }
    }
    1 Beanクラスに画面からの入力値をマッピングするためのユーティリティクラスを初期化します。
    ※ whitelistPropertyNameRegexメソッドにセット可能な項目名の正規表現式を指定します。
    2 多言語利用で「日本語」が選択されているまたは多言語利用の設定が取得できなかった場合、samples.ec01.bean.validator.group.JapaneseChecks グループとデフォルトグループに属する項目に対してバリデーションが実行されます。
    ※ populate(Object, Map, Class)メソッドはスレッドセーフですが、それ以外の delimiters(char, char, char)等の設定用メソッドがスレッドセーフではありません。 詳しい説明は org.iplass.mtp.command.beanmapper.BeanParamMapper のJavaDocを参照してください。
    3 多言語利用で「日本語」以外が選択されている場合、デフォルトグループに属する項目のみに対してバリデーションが実行されます。
    4 MappingExceptionが発生した場合、それをRequestスコープにセットします。iPLAssが自動にJSON形式に変換してクライアントに返却してくれます。

    このサンプルでJSON形式に変換されたMappingExceptionの例です。それがVue.js側で解析できるようになっています。

    {"status":"ERROR","result":{"errors":[{"propertyPath":"mail","errorMessages":["値を入力してください。"]},{"propertyPath":"familyNameKana","errorMessages":["値を入力してください。"]},{"propertyPath":"firstNameKana","errorMessages":["値を入力してください。"]},{"propertyPath":"content","errorMessages":["値を入力してください。"]},{"propertyPath":"familyName","errorMessages":["値を入力してください。"]},{"propertyPath":"firstName","errorMessages":["値を入力してください。"]}]}}
  • Vue.jsの部分

    ファイル名

    /src/main/vue/components/inquiry/RegistInquiry.vue

    ----------------------------------------以上略----------------------------------------
        <form class="custom-form mt-3">
            <div class="form-group row">
            ......
                <div class="col-12 col-md-6 mt-3">
                    <div>
                        <label for="familyNameKana" class="col-form-label label-hidden">{{$t("samples.ec01.inquiry.regist.familyNameKana")}}</label>
                        <input type="text" class="form-control border rounded input-hint-visible" name="familyNameKana" v-model="inquiryBean.familyNameKana" v-bind:placeholder="$t('samples.ec01.inquiry.regist.familyNameKana')">
                        <small class="form-text text-danger"><template v-for="message in errorsMap.familyNameKana">{{message}}<br v-bind:key="message"/></template></small> (1)
                    </div>
                </div>
            ......
        </form>
    ----------------------------------------以下略----------------------------------------
    1 返却されたバリデーションエラーメッセージを画面に出力します。
  • 動作確認

    • 「姓」と「名」を空文字として登録しようとしたら、バリデーションエラーが発生することを画面から確認できます。

    • 「セイ」と「メイ」に全角カタカナ以外の値を入れて登録しようとしたら、バリデーションエラーが発生することを画面から確認できます。

      sample ec vuejs webapi bean validation error

    • 多言語利用で「英語」が選択された場合、英語のバリデーションエラーメッセージが表示されることを確認できます。

      sample ec vuejs webapi bean validation error en

      ※ 英語用の画面にカタカナの「セイ」と「メイ」の入力項目がないので、日本語用の画面と比べてレイアウトに少し違いがあります。

3.3. Entity

  • Admin ConsoleのEntity定義で Mapping Class 機能の利用でEntityのJavaクラスを自動生成することができます。

    パッケージ

    samples.ec01.entity

  • Entity多言語対応をしています。
    Admin Console部分の設定でEntity多言語対応をご参照してください。

3.4. 共通エラーハンドラー

Vue.js/WebApi版のサンプルでは画面遷移がクライアント側で行っているので、Java/JSP版とGroovy/GroovyTemplate版のサンプルで利用されているErrorUrlSelector機能が使えませんので、axios ライブラリのinterceptors機能を利用して似たような機能を実装しています。

ファイル名

/src/main/vue/main.js

// Axiosの設定
Vue.prototype.$http = axios.create({
    headers: {'X-Requested-With': 'XMLHttpRequest'} (1)
});
----------------------------------------以上略----------------------------------------
const app = new Vue({
  el: '#app',
  i18n: i18n,
  router: router,
  methods: {
    setupAxiosErrorInterceptors: function() { (2)
      this.$http.interceptors.response.use((response) => {
        return response;
      }, (error) => {
        console.log(error);
        var errorResult = error.response.data;
        if (errorResult.exceptionType != null) {
          var exception = errorResult.exceptionType;
          this.$router.push({name: 'genericError', params: {'exception': exception}}); (3)
        }
        return Promise.reject(error);
      })
    }
  },
  created: function() {
    // axiosにインターセプターを設定
    this.setupAxiosErrorInterceptors();
  }
});
----------------------------------------以下略----------------------------------------
1 JSONハイジャックによる攻撃を防ぎます。セキュリティ対策を参照してください。
2 Vue.js側で axios にerror interceptorを登録します。
3 WebApi側でエラーが発生した場合、表示画面がVue.js側で genericError という名前で登録されたVueコンポーネントに切り替えられます。

axiosvue-router ライブラリの使い方について、公式サイトを参照してください。

動作確認

TokenValidationExceptionが発生した場合、カスタムのテンプレートに遷移すること。

  • Consoleに出力されたエラーログ。

    14:26:53.626 [http-nio-8080-exec-6] DEBUG 25 873   o.i.mtp.impl.auth.AuthContextHolder - check WebApiPermission [webApiName=samples/ec01/inquiry/doInquiry, parameter=org.iplass.mtp.webapi.permission.RequestContextWebApiParameter@1b1910ae] = true (privilegedExecution)
    14:26:53.626 [http-nio-8080-exec-6] DEBUG 25 873 samples/ec01/inquiry/RegistInquiryCommand  o.i.m.i.transaction.LocalTransaction - create new Transaction:org.iplass.mtp.impl.transaction.LocalTransaction@791691ea with readOnly=false, stacked:null
    14:26:53.626 [http-nio-8080-exec-6] DEBUG 25 873 samples/ec01/inquiry/RegistInquiryCommand  o.iplass.mtp.transaction.Transaction - rollback transaction cause org.iplass.mtp.web.actionmapping.TokenValidationException: 不正な画面遷移が発生しました(一連の登録処理中にブラウザの戻るボタン等を押下してしまいますと正常に処理を継続できない場合があります)。:org.iplass.mtp.impl.transaction.LocalTransaction@791691ea
    org.iplass.mtp.web.actionmapping.TokenValidationException: 不正な画面遷移が発生しました(一連の登録処理中にブラウザの戻るボタン等を押下してしまいますと正常に処理を継続できない場合があります)。
        at org.iplass.mtp.impl.webapi.interceptors.TokenInterceptor.tokenError(TokenInterceptor.java:96)
        at org.iplass.mtp.impl.webapi.interceptors.TokenInterceptor.intercept(TokenInterceptor.java:76)
        at org.iplass.mtp.impl.command.InvocationImpl.proceedCommand(InvocationImpl.java:115)
        at org.iplass.mtp.impl.command.interceptors.TransactionInterceptor.lambda$intercept$0(TransactionInterceptor.java:34)
        at org.iplass.mtp.transaction.TransactionManager.doTransaction(TransactionManager.java:114)
        at org.iplass.mtp.transaction.Transaction.with(Transaction.java:303)
        at org.iplass.mtp.impl.command.interceptors.TransactionInterceptor.intercept(TransactionInterceptor.java:33)
        at org.iplass.mtp.impl.command.InvocationImpl.proceedCommand(InvocationImpl.java:115)
    ----------------------------------------以下略----------------------------------------
  • クライアントに返却されたレスポンス。

    {"status":"FAILURE","exceptionType":"org.iplass.mtp.web.actionmapping.TokenValidationException","exceptionMessage":"不正な画面遷移が発生しました(一連の登録処理中にブラウザの戻るボタン等を押下してしまいますと正常に処理を継続できない場合があります)。"}
  • エラー内容表示画面

    sample ec vuejs webapi interceptor token error

3.5. ReportOutput

Vue.js版の注文明細ダウンロード機能は、Java/JSP版のReportOutputと同じ実装になっていますので、そちらの説明を参照してください。

3.6. WebApiとの連携

Ajaxを利用することで、WebApiと連携することが出来ます。一般消費者向け画面で全文検索処理を例として説明していきます。

  • 開発者ガイドWebApiの章を参照してください。
    以下はここで利用しているWebApiです。

    WebApi名

    samples/ec01/search/fulltextSearch

  • WebApiを利用しているVueコンポーネント

    ファイル名

    /src/main/vue/components/search/FullTextSearch.vue

    ----------------------------------------以上略----------------------------------------
            fullTextSearch: function() {
                if (this.productName == "") {
                    this.helpMessage = this.$t('samples.ec01.search.nokeyword');
                    return false;
                }
                var url = this.apiFulltextSearch();
                var data = {productName: this.productName, categoryOid: this.categoryOid};
                this.$http.post(url, data) (1)
                    .then((response) => {
                        var commandResult = response.data;
                        if(commandResult.status == 'SUCCESS') {
                            if(commandResult.defaultResult != null && commandResult.defaultResult.length > 0){
                                this.fullSearchResult = this.ListSearchResult(commandResult.defaultResult, this.productName); (2)
                                $('#searchResultDiv').html(this.fullSearchResult);
                            }
                            else{
                                this.helpMessage = this.$t('samples.ec01.search.keyword') + this.productName + ", " + this.$t('samples.ec01.search.noResult');
                            }
                        } else {
                            console.log(response);
                        }
                    })
                    .catch((error) => { (3)
                        var errorResult = error.response.data;
                        if (errorResult.exceptionType != null) {
                            alert(this.$t('samples.ec01.search.jsError') + errorResult.exceptionType + "\n" + errorResult.exceptionMessage);
                            return;
                        }
                    });
            },
            dropdownSelect: function(event) {
                var t = $(event.target);
                var v = t.attr("category-item-value");
                $("#categoryList").text(t.html()).attr("category-item-selected", v);
            },
            ListSearchResult: function(entities, productName){ (4)
                var yen = this.$t('samples.ec01.all.yen');
                var html =  "<div class=\"col-12 mb-2\">";
                    html += "   <h4>" + this.$t('samples.ec01.search.result') + productName + "</h4>";
                    html += "</div>";
                for(var i =0; i < entities.length; i++){
                    var name = entities[i].name;
                    var price = isNaN(entities[i].price)? "" : entities[i].price;
                    var imageUrl = this.imgUrl(entities[i].productImg);
                    var detailUrl = "#/product/detail?productId=" + entities[i].oid;
                    html += "<div class=\"col-12 col-md-4\">";
                    html += "   <div class=\"card border-light border-0\">";
                    html += "       <a href=\"" + detailUrl + "\" class=\"h-100\">";
                    html += "          <img class=\"card-img-top img-thumbnail img-fluid all-product-img\" src=" + imageUrl + " alt=\"" + name + "\">";
                    html += "       </a>";
                    html += "       <div class=\"card-body pt-md-1 text-center\">";
                    html += "          <div>";
                    html += "              <a href=\""+ detailUrl +"\" class=\"card-link text-dark\">" + name + "</a>";
                    html += "           </div>";
                    html += "           <div class=\"all-price\">";
                    html += "              <span>" + price + "</span>" + yen;
                    html += "           </div>";
                    html += "       </div>";
                    html += "   </div>";
                    html += "</div>";
                }
                return html;
            }
    ----------------------------------------以下略----------------------------------------
    1 WebApiによる検索処理を呼び出します。
    2 返された検索処理の結果を取得し、クライアント側の描画処理を呼び出します。
    3 検索処理でエラーが発生した場合、クライアント側での処理。
    4 検索結果の描画処理。
  • 動作確認

    動作結果はJava/JSP版と同じなので、そちらの動作確認画面を参照してください。

3.7. セキュリティ対策

ここでは会員登録を例としてサンプルアプリで利用されているセキュリティ対策について説明します。

  1. エスケープ機能 ※  XSS対策 : ユーザーの入力内容を正常に画面表示させる

  2. TokenCheck機能  CSRF対策/トランザクション重複起動対策

  3. Navigation Guards ※  画面表示時に正常な画面遷移が行われているかをチェックする

  4. check X-Requested-With Header  JSONハイジャックによる攻撃を防ぎます

会員登録処理の中でそれぞれの機能は下記のように利用されます。
※ Vue.jsの機能でiPLAssの機能ではありません。

  • エスケープの導入 ※
    画面の作成
    入力された値を出力する会員情報確認画面にて、該当箇所にVue.jsの {{変数名}} エスケープ処理を導入する。
    Vuew.jsの公式サイトを参照してください。

    ファイル名

    /src/main/vue/components/member/RegistConfirm.vue

    ----------------------------------------以上略----------------------------------------
        <div class="card col-12 bg-light">
            <div class="card-body">
                <div class="row mt-3 border-bottom">
                    <div class="col-12 col-md-4">
                        <span class="text-muted font-weight-bold">{{$t("samples.ec01.member.regist.userId")}}</span>
                    </div>
                    <div class="col-12 col-md-8">{{userBean.userId}}</div> (1)
                </div>
                <div class="row mt-3 border-bottom">
                    <div class="col-12 col-md-4">
                        <span class="text-muted  font-weight-bold">{{$t("samples.ec01.member.registConfirm.fullName")}}</span>
                    </div>
                    <div class="col-12 col-md-3">
                        <span class="text-muted  font-weight-bold">{{$t("samples.ec01.member.regist.familyName")}}</span>
                        &nbsp;{{userBean.familyName}} (1)
                    </div>
                    <div class="col-12 col-md-3">
                        <span class="text-muted  font-weight-bold">{{$t("samples.ec01.member.regist.firstName")}}</span>
                        &nbsp;{{userBean.firstName}} (1)
                    </div>
                </div>
                <div class="row mt-3 border-bottom" v-if="locale == 'ja' || locale === undefined">
                    <div class="col-12 col-md-4">
                        <span class="text-muted  font-weight-bold">{{$t("samples.ec01.member.registConfirm.fullNameKana")}}</span>
                    </div>
                    <div class="col-12 col-md-3">
                        <span class="text-muted  font-weight-bold">{{$t("samples.ec01.member.regist.familyNameKana")}}</span>
                        &nbsp;{{userBean.familyNameKana}} (1)
                    </div>
                    <div class="col-12 col-md-3">
                        <span class="text-muted  font-weight-bold">{{$t("samples.ec01.member.regist.firstNameKana")}}</span>
                        &nbsp;{{userBean.firstNameKana}} (1)
                    </div>
                </div>
                <div class="row mt-3 border-bottom">
                    <div class="col-12 col-md-4">
                        <span class="text-muted  font-weight-bold">{{$t("samples.ec01.member.regist.mail")}}</span>
                    </div>
                    <div class="col-12 col-md-8">{{userBean.mail}}</div> (1)
                </div>
            </div>
        </div>
    ----------------------------------------以下略----------------------------------------
    1 今回は出力先がHTML形式であるため全てVue.jsの{{変数名}}という形式でエスケープを行っている。
  • TokenCheckの導入
    この機能では、ワンタイムトークン方式を利用することでCSRFとトランザクションの重複実行(ダブルサブミット)を防ぐことができます。 開発者ガイドToken Checkの章を参照してください。

    • WebApiの設定
      不正な画面遷移を禁止するWebApiでTokenをチェックする設定を行います。

      会員情報確認画面アクション(WebApiのsamples/ec01/member/confirmMemberInfoを参照)

      @WebApi(
              name = "samples/ec01/member/confirmMemberInfo",
              displayName = "会員情報確認アクション",
              accepts = RequestType.REST_JSON,
              restJson = @RestJson(parameterName = "param"),
              methods = MethodType.POST,
              privileged = true,
              tokenCheck = @WebApiTokenCheck( (1)
                      executeCheck = true,
                      consume = true,
                      exceptionRollback = true),
              results = {
                      ConfirmMemberInfoCommand.RESULT_MAPPING_RESULT,
                      ConfirmMemberInfoCommand.RESULT_MEMBER_AGREE })
      @CommandClass(
              name = "samples/ec01/member/ConfirmMemberInfoCommand",
              displayName = "会員情報確認コマンド")
      public class ConfirmMemberInfoCommand implements Command {

      ----------------------------------------以下略----------------------------------------

      1 WebApiの定義にtokenCheckアノテーションを利用しています。

      以下の設定が可能です。

      executeCheck

      false チェックを行わない true チェックを行う

      useFixedToken

      チェック→セッション単位に固定に払いだされるTokenをチェックする

      consume

      true→Tokenは再利用されません。

      exceptionRollback

      チェック→現在のTokenを再設定

      この設定によりトークンチェックに失敗した場合、以下のJSON形式のエラーメッセージが返却されます。

      返却の例

      {"status":"FAILURE","exceptionType":"org.iplass.mtp.web.actionmapping.TokenValidationException","exceptionMessage":"不正な画面遷移が発生しました(一連の登録処理中にブラウザの戻るボタン等を押下してしまいますと正常に処理を継続できない場合があります)。"}
    • Java/JSP版サンプルのように、iPLAss既存のトークン出力JSPタブを利用することができないので、似たような機能を実現するために、以下の対応を行っています。

      1. トークン出力用WebApiの作成

        Command名

        samples.ec01.command.token.OutputToken

        @WebApi(
                name = "samples/ec01/token/outputToken",
                displayName = "",
                accepts = RequestType.REST_JSON,
                methods = MethodType.POST,
                privileged = true,
                synchronizeOnSession = true,
                results = {
                        OutputToken.RESULT_TOKEN_NAME,
                        OutputToken.RESULT_TOKEN_VALUE })
        @CommandClass(
                name = "samples/ec01/token/outputToken",
                displayName = "トークン出力コマンド")
        public class OutputToken implements Command {
        
            public static final String RESULT_TOKEN_NAME = "tokenName";
            public static final String RESULT_TOKEN_VALUE = "tokenValue";
        
            @Override
            public String execute(RequestContext request) {
                String value = TemplateUtil.outputToken(TokenOutputType.VALUE, true); (1)
                request.setAttribute(RESULT_TOKEN_NAME, TokenStore.TOKEN_PARAM_NAME); (2)
                request.setAttribute(RESULT_TOKEN_VALUE, value); (3)
        
                return Constants.CMD_EXEC_SUCCESS;
            }
        }
        1 トークンを生成します。
        2 トークンパラメータ名をrequestスコープにセットして返却します。
        3 トークンの値をrequestスコープにセットして返却します。
      2. トークン取得用Vueコンポーネント

        トークン取得用Vueコンポーネントを作成しています。それをトークンチェックに必要な画面にインポートする形で利用してます。

        ファイル名

        /src/main/vue/components/token/OutputToken.vue

        <script>
        import {Consts} from '../../mixins/Consts'
        
        export default {
            name: 'OutputToken',
            mixins: [Consts],
            data: function() {
                return {
                    token: {
                        name: "",
                        value: ""
                    }
                }
            },
            methods: {
                // トークンを取得する
                get: function() {
                    return this.token;
                },
                // トークンをリロードする
                reload: function() {
                    var url = this.apiOutputToken();
                    var data = {};
                    this.$http.post(url, data) (1)
                    .then((response) => {
                        var commandResult = response.data;
                        if (commandResult.status == 'SUCCESS'){
                            this.token.name = commandResult.tokenName; (2)
                            this.token.value = commandResult.tokenValue; (3)
                        }
                    });
                }
            },
            created: function() {
                this.reload();
            }
        }
        </script>
        1 VueコンポーネントでWebApiを経由してトークンを取得してます。
        2 トークンのパラメーター名を取得します。
        3 トークンの値を取得します。
  • Navigation Guards ※

    ※ Navigation Guardsの利用方法について、Vue Routerの公式サイトを参照してください。

    項目 設定内容

    In-Component Navigation Guards(Vue Router)

    画面表示時に正常な画面遷移が行われているかをチェックする

    会員情報確認画面を例として説明して行きます。

    ファイル名

    src/main/vue/components/member/RegistConfirm.vue

      beforeRouteEnter: function(to, from, next) { (1)
        // 不正な画面遷移が発生したと判断
        if(['regist'].indexOf(from.name) == -1 || to.params.userBean === undefined) {
          next(new Error('samples.ec01.exception.invalidTransition'));
        } else {
          next();
        }
      }
    1 Vueコンポーネントに beforeRouteEnter メソッドを利用することで、正常な画面遷移であるか判断する事が出来ます。
  • check X-Requested-With Header

    項目 設定内容

    check X-Requested-With Header

    JSONハイジャックによる攻撃を防ぎます。

    リクエストヘッダーに’X-Requested-With’の項目を追加します。

    ファイル名

    /src/main/vue/main.js

    ----------------------------------------以上略----------------------------------------
    // Axiosの設定
    Vue.prototype.$http = axios.create({
        headers: {'X-Requested-With': 'XMLHttpRequest'} (1)
    });
    ----------------------------------------以下略----------------------------------------
    1 詳しい説明は開発者ガイド設定の章を参照してください。
    • エラー発生の例

      ブラウザーのURL欄にWebApiのsamples/ec01/topをアクセスすると、WebApi側で不正なアクセスとして検知され、以下のエラーが発生します。

      18:57:19.053 [http-nio-8080-exec-28] DEBUG -1    o.i.m.i.r.c.LocalTransactionConnectionWrapper - back to ResourceHolder:1552070718, URL=jdbc:mysql://[host]:[port]/[schema]
      18:57:19.055 [http-nio-8080-exec-28] DEBUG -1    o.i.m.impl.command.MetaSingleCommand - init Command instance:samples.ec01.command.TopCommand@258e40ca
      18:57:19.055 [http-nio-8080-exec-28] DEBUG -1    o.i.mtp.impl.core.ExecuteContext - finalize execute context:org.iplass.mtp.impl.core.ExecuteContext@28dc8ad9
      18:57:19.069 [http-nio-8080-exec-28] ERROR 25    o.i.m.i.w.rest.MtpExceptionMapper - unhandle excepion on web api call:org.iplass.mtp.webapi.WebApiRuntimeException: X-Requested-With Header( or Custom Header) is needed on WebApi:samples/ec01/top
      org.iplass.mtp.webapi.WebApiRuntimeException: X-Requested-With Header( or Custom Header) is needed on WebApi:samples/ec01/top
          at org.iplass.mtp.impl.webapi.MetaWebApi$WebApiRuntime.checkXRequestedWith(MetaWebApi.java:483)
          at org.iplass.mtp.impl.webapi.rest.RestCommandInvoker.checkValidRequest(RestCommandInvoker.java:196)
          at org.iplass.mtp.impl.webapi.rest.RestCommandInvoker.lambda$doGet$1(RestCommandInvoker.java:320)
          at org.iplass.mtp.impl.webapi.rest.RestCommandInvoker.process(RestCommandInvoker.java:130)
          at org.iplass.mtp.impl.webapi.rest.RestCommandInvoker.doGet(RestCommandInvoker.java:316)
          at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

3.8. 多言語対応

多言語を利用するために、サンプルアプリで以下の対応を行っています。

4. リソース定義

4.1. メタデータ定義

  • コマンドのメタデータ定義。
    ここではWebApiのコマンドをメタデータとして登録しています。アプリの起動時に読み込まれます。

    コマンドのメタデータ定義ファイル

    ファイル名

    /src/main/java/samples/ec01/command/metadata/CommandList.java

  • 他のメタデータ定義

    ファイル名

    /src/main/resources/samples-ec01-ce-metadata.xml

    ここでは二種類のメタデータが用意されています。

    metaDataList
        ┣ contextPath (name=/entity) (1)
        ┣ contextPath (name=/property/select) (1)
        ┣ contextPath (name=/template) ※ 帳票出力機能と管理トップ画面 (1)
        ┣ contextPath (name=/view/calendar) (1)
        ┣ contextPath (name=/view/generic) (1)
        ┣ contextPath (name=/view/menu/item) (1)
        ┣ contextPath (name=/view/menu/tree) (1)
        ┣ contextPath (name=/view/top) (1)
        ┣ contextPath (name=/view/treeview) (1)
        ┣ contextPath (name=/view/filter) (1)
        ┗ contextPath (name=/template) (2)
    1 Admin Consoleで作成したメタデータです。Packaging機能もしくはMetaDataExplorer機能を利用することでエクスポートしたものです。
    2 Templateのメタデータ定義は手で作成しています。
    ※ 帳票出力機能と管理トップ画面のTemplate定義はAdmin Consoleで作成し、エクスポートしています。
    ※ このサンプルではクライアント側の画面がVue.jsで実装されているので、管理用の在庫一括更新画面のTemplate定義のみメタデータに登録する必要があります。

    手で作成したTemplate定義。

    ----------------------------------------以上略----------------------------------------
    <contextPath name="/template">
        <!-- Backoffice pages start -->
        <metaDataEntry>
            <metaData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:type="metaJspTemplate">
                <name>samples/ec01/backoffice/stock/stockUpdate</name>
                <displayName>在庫一括更新画面</displayName>
                <path>/jsp/samples/ec01/backOffice/stock/stockUpdate.jsp</path>
                <layoutId>/action/gem/layout/defaultLayout</layoutId>
                <contentType>text/html; charset=utf-8</contentType>
            </metaData>
        </metaDataEntry>
        <!-- Backoffice pages end -->
    </contextPath>
    ----------------------------------------以下略----------------------------------------

4.2. 多言語メッセージファイル

  • vue-i18n に利用する多言語メッセージファイルを定義しています。vue-i18nの公式サイトを参照してください。

    ファイル名

    /src/main/vue/scripts/iplass-wtp-messages.json

    {
        "en": {
            "samples": {
                "ec01": {
                    "all": {
                        "breadcrumb": {
                            "home": "Home" (1)
                        },
                        "pagination": {
                            "next": "Next",
                            "prev": "Prev"
                        },
                        "yen": "Yen"
                    },
                ......
                }
            }
        },
        "ja": {
            "samples": {
                "ec01": {
                    "all": {
                        "breadcrumb": {
                            "home": "ホーム" (1)
                        },
                        "pagination": {
                            "next": "次へ",
                            "prev": "前へ"
                        },
                        "yen": ""
                    },
                ......
                }
            }
        }
    }
    1 言語別にラベルメッセージを用意しています。
  • 利用例

    <template>
    <div class="col-sm-12 col-md-9">
        <div class="row">
            <div class="col-12">
                <div class="border-top"></div>
                <nav class="breadcrumb all-breadcrumb">
                    <router-link class="breadcrumb-item text-primary" v-bind:to="{name: 'top'}">{{$t("samples.ec01.all.breadcrumb.home")}}</router-link> (1)
                    <span class="breadcrumb-item active">{{categoryNameLocale}}</span>
                </nav>
            </div>
        </div>
        ......
    </template>
    1 {{$t(変数名)}} の形式で多言語メッセージファイルからメッセージを取得できます。

4.3. Bean Validationメッセージ

  • 言語別にプロパティファイルを作成することで多言語利用が可能です。

    ファイル名

    /src/main/resources/ValidationMessages_ja.properties

    samples.ec01.bean.validation.Tel.invalidChar    = 「数字」および「-」のみ利用可能です。(1)
    samples.ec01.bean.validation.UserId.invalidChar = 「英数字」および「-」(ハイフン)「@」「_」「.」(ピリオド)のみ利用可能です。
    samples.ec01.bean.validation.UserId.outOfLength = ユーザーIDは{min}文字以上{max}文字以下です (2)
    samples.ec01.bean.validation.isBlank            = 値を入力してください。
    samples.ec01.bean.validation.notKana            = 全角カタカナを入力してください。
    ......

    ファイル名

    /src/main/resources/ValidationMessages_en.properties

    samples.ec01.bean.validation.Tel.invalidChar    = Please enter alphanumeric characters or "-"(hypen) . (1)
    samples.ec01.bean.validation.UserId.invalidChar = Please enter alphanumeric characters or "-"(hypen), "@", "_"(underscore), "."(dot) .
    samples.ec01.bean.validation.UserId.outOfLength = User ID must be in range between {min} and {max} characters. (2)
    samples.ec01.bean.validation.isBlank            = Please enter value.
    samples.ec01.bean.validation.notKana            = Please enter Katakana.
    ......
    1 多言語を利用するので、英語と日本語のバリデーションエラーメッセージを用意しています。
    2 メッセージにパラメーターの利用が可能です。