4.0
Vue.js/WebAPI版

1. 概要

Vue.js/WebApiを利用して実装されたサンプルアプリケーションです。
Vue.jsの説明については、公式ドキュメントを参照してください。
Vue.js以外に、vue-routervue-i18naxios といったライブラリを利用しています。

2. セットアップ

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

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

    サンプルコードで Java21 より上のバージョンのスタイルでコーディングする場合は build.gradle の javaVersion を使用したい Java のバージョンに書き換えてください。
    書き換えた後はGradleプロジェクトのリフレッシュを行い、エラーが出ないことを確認してください。
    Vue Component のビルドに利用する Node.js のバージョンは、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_21 (1)
        nodejsVersion = '20.13.1' (2)
    }
    
    ----------------------------------------以下略----------------------------------------
    1 必要に応じて使用したい Java のバージョンに書き換えます。
    2 ビルドに利用する Node.js のバージョンを指定します。
  • サンプルアプリを起動する前に、プロジェクトルートから以下のコマンドを用いてVue.jsのコンポーネントを事前にビルドしておく必要があります。

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

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

3. 機能

3.1. Top画面の作成

  • レイアウトの利用
    Top画面の構成は下図のようになっています。画面共通で利用する2つのレイアウト用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-flush">
                  <router-link
                    :to="{ name: 'top' }"
                    class="list-group-item list-group-item-action fw-bold border-top"
                  >
                    {{ $t('samples.ec01.layout.defaultLayout.home') }}
                  </router-link>
                  <template v-for="category in categoryList" :key="category.oid">
                    <router-link
                      :to="{ name: 'category', query: { categoryId: 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部分の説明は割愛します。詳細は、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メソッドはスレッドセーフですが、それ以外の delimitersなどの設定用メソッドはスレッドセーフではありません。 詳しい説明は、 org.iplass.mtp.command.beanmapper.BeanParamMapper のJavaDocを参照してください。
    3 多言語利用で「日本語」以外が選択されている場合、デフォルトグループに属する項目のみに対してバリデーションが実行されます。
    4 MappingExceptionが発生した場合、Requestスコープに WebRequestConstants.EXCEPTION をキーにMappingExceptionが格納されます。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
                        v-model="inquiryBean.familyNameKana"
                        type="text"
                        class="form-control border rounded input-hint-visible"
                        name="familyNameKana"
                        :placeholder="$t('samples.ec01.inquiry.regist.familyNameKana')"
                      />
                      <small class="form-text text-danger">
                        <template v-for="message in errorsMap.familyNameKana" :key="message">
                          {{ message }}<br /> (1)
                        </template>
                      </small>
                    </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の設定
app.config.globalProperties.$http = axios.create({
  headers: { 'X-Requested-With': 'XMLHttpRequest' } (1)
})
----------------------------------------以上略----------------------------------------
const app = createApp(App, {
  onExpand() {},
  created() {
    // axiosにインターセプターを設定
    this.setupAxiosErrorInterceptors()
  },
  methods: {
    setupAxiosErrorInterceptors() { (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)
        }
      )
    }
  }
})
----------------------------------------以下略----------------------------------------
1 JSONハイジャックによる攻撃を防ぎます。セキュリティ対策を参照してください。
2 Vue.js側で axios にerror interceptorを登録します。
3 WebApiでエラーが発生した場合、Vue.js側で genericError という名前で登録されたVueコンポーネントに切り替えます。

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

動作確認

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

  • コンソールに出力されるエラーログは以下の通りです。

    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/WebAPI版の注文明細ダウンロード機能は、Java/JSP版のReportOutputと同じ実装になっていますので、そちらの説明を参照してください。

3.6. WebApiとの連携

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

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

    WebApi名

    samples/ec01/search/fulltextSearch

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

    ファイル名

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

    ----------------------------------------以上略----------------------------------------
        fullTextSearch() {
          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.transformSearchResults(commandResult.defaultResult) (2)
                  this.helpMessage = ''
                } else {
                  this.helpMessage =
                    this.$t('samples.ec01.search.keyword') +
                    this.productName +
                    ', ' +
                    this.$t('samples.ec01.search.noResult')
                  this.fullSearchResult = []
                }
              } 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(category) {
          if (typeof category === 'string') {
            // "all" を選択した場合
            this.selectedCategory = { oid: 'all', name: this.$t('samples.ec01.product.category.all') }
          } else {
            // カテゴリを選択した場合
            this.selectedCategory = category
          }
        },
        transformSearchResults(entities) { (4)
          return entities.map((entity) => ({
            oid: entity.oid,
            name: entity.name,
            price: isNaN(entity.price) ? '' : entity.price,
            imageUrl: this.imgUrl(entity.productImg),
            detailUrl: `#/product/detail?productId=${entity.oid}`
          }))
        }
      }
    ----------------------------------------以下略----------------------------------------
    1 WebApiによる検索処理を呼び出します。
    2 返された検索処理の結果を取得し、クライアント側への反映処理を呼び出します。
    3 検索処理でエラーが発生した場合、クライアント側での処理。
    4 検索結果への反映処理。
  • 動作確認

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

3.7. セキュリティ対策

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

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

  2. CSRF対策/トランザクション重複起動対策となるトークンチェック機能 : 画面表示時に正常な画面遷移が行われているかをチェックする

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

  4. X-Requested-Withヘッダーのチェック : JSONハイジャックによる攻撃を防ぎます

会員登録処理の中でそれぞれの機能は下記のように利用されます。

  • エスケープの導入
    入力された値を出力する会員情報確認画面にて、該当箇所にVue.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 fw-bold">
                          {{ $t('samples.ec01.member.regist.userId') }}
                        </span>
                      </div>
                      <div class="col-12 col-md-8">{{ localUserBean.userId }}</div> (1)
                    </div>
                    <div class="row mt-3 border-bottom">
                      <div class="col-12 col-md-4">
                        <span class="text-muted fw-bold">
                          {{ $t('samples.ec01.member.registConfirm.fullName') }}
                        </span>
                      </div>
                      <div class="col-12 col-md-3">
                        <span class="text-muted fw-bold">
                          {{ $t('samples.ec01.member.regist.familyName') }}
                        </span>
                        &nbsp;{{ localUserBean.familyName }} (1)
                      </div>
                      <div class="col-12 col-md-3">
                        <span class="text-muted fw-bold">
                          {{ $t('samples.ec01.member.regist.firstName') }}
                        </span>
                        &nbsp;{{ localUserBean.firstName }} (1)
                      </div>
                    </div>
                    <div v-if="locale == 'ja' || locale === undefined" class="row mt-3 border-bottom">
                      <div class="col-12 col-md-4">
                        <span class="text-muted fw-bold">
                          {{ $t('samples.ec01.member.registConfirm.fullNameKana') }}
                        </span>
                      </div>
                      <div class="col-12 col-md-3">
                        <span class="text-muted fw-bold">
                          {{ $t('samples.ec01.member.regist.familyNameKana') }}
                        </span>
                        &nbsp;{{ localUserBean.familyNameKana }} (1)
                      </div>
                      <div class="col-12 col-md-3">
                        <span class="text-muted fw-bold">
                          {{ $t('samples.ec01.member.regist.firstNameKana') }}
                        </span>
                        &nbsp;{{ localUserBean.firstNameKana }} (1)
                      </div>
                    </div>
                    <div class="row mt-3 border-bottom">
                      <div class="col-12 col-md-4">
                        <span class="text-muted fw-bold">
                          {{ $t('samples.ec01.member.regist.mail') }}
                        </span>
                      </div>
                      <div class="col-12 col-md-8">{{ localUserBean.mail }}</div> (1)
                    </div>
                  </div>
                </div>
    ----------------------------------------以下略----------------------------------------
    1 今回は出力先がHTML形式であるためすべてVue.jsの{{ 変数名 }}という形式でエスケープを行っています。
  • トークンチェックの導入
    この機能では、ワンタイムトークン方式を利用することでCSRFとトランザクションの重複実行(ダブルサブミット)を防ぐことができます。
    詳細は、開発者ガイドのToken Checkの章を参照してください。

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

      会員情報確認画面アクション(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

      トークンチェックを行うかどうか

      useFixedToken

      セッション単位に固定に払いだされるトークンを利用するか

      consume

      トークンを消費するか

      exceptionRollback

      Exception発生時にトークンをロールバックするか

      この設定によりトークンチェックに失敗した場合、以下の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() {
            return {
              token: {
                name: '',
                value: ''
              }
            }
          },
          created() {
            this.reload()
          },
          methods: {
            // トークンを取得する
            get() {
              return this.token
            },
            // トークンをリロードする
            reload() {
              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)
                }
              })
            }
          }
        }
        </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

      beforeRouteUpdate(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 メソッドを利用することで、正常な画面遷移であるか判断することができます。
  • X-Requested-Withヘッダーのチェック

    項目 設定内容

    check X-Requested-With Header

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

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

    ファイル名

    /src/main/vue/main.js

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

      ブラウザーの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

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

    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" :to="{ name: 'top' }">
                {{ $t('samples.ec01.all.breadcrumb.home') }} (1)
              </router-link>
              <span :id="categoryId" class="breadcrumb-item active">{{ categoryNameLocale }}</span>
            </nav>
          </div>
        </div>
        ......
    </template>
    1 Vue.jsのテンプレートで、 {{ $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 メッセージにパラメーターの利用が可能です。