広告

■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
                      2010年05月16日

    Java総合講座 - 初心者から達人へのパスポート
                  vol.204

                                セルゲイ・ランダウ
 バックナンバー: http://www.flsi.co.jp/Java_text/
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■


-------------------------------------------------------
・現在、このメールマガジンは以下の2部構成になっています。
[1] 当初からのコース:毎週日曜の夜に発行
   これは現在、中級レベルになっています。
[2] 2009年11月開講コース:毎週水曜の夜に発行
   これは現在、初心者向けのレベルになっています。
・このメールマガジンは、画面を最大化して見てください。
小さな画面で見ていると、不適切な位置で行が切れてしまう
など、問題を起すことがあります。
・このメールマガジンに掲載されているソース・コード及び
文章は特に断らない限り、すべて筆者が著作権を所有してい
ます。また、これらのソース・コードは学習用のためだけに
提供しているものです。
-------------------------------------------------------


========================================================
◆ 01.Strutsのアプリケーション開発(プロジェクト:StrutsShop)
========================================================


Strutsでは、ボタンの二度押し等でサーバーに重複して2回以上同じ要求をしてしまう
ことを防止するために、トランザクション・トークン(transaction token)と呼ばれる
機能を提供しています。

(トランザクションが何であるか忘れた人は、vol.023(もしくは2009年11月開講コース
の022号)を復習のこと。)

トークン(token)の本来の意味は、アメリカの地下鉄などで改札口を通過するときに
使うコイン型の切符のことで、これを装置に入れることによって改札口を通過する
ことができます。
したがってトークンは、いわば通行証のような働きをするものです。

これと同じく、トランザクション・トークンはトランザクションを進行(実行)させる
ための通行証のような働きをするものを意味します。
ただし、地下鉄のトークンとは異なり、トランザクション・トークンは特定の
トランザクションの実行を1回だけしか要求できないように制御するために使われ
ます。

これは、Actionクラスに提供されている下記のようなメソッドを使って実現されます。

(1) void saveToken(HttpServletRequest request)
      トークンを作成しセッションに保管します。
(2) boolean isTokenValid(HttpServletRequest request)
      トークンを検証します。
(3) void resetToken(HttpServletRequest request)
      トークンをリセットします。


まず、一つのActionクラスの中で呼び出された(1)のメソッドはトークンを生成し、
セッション・オブジェクトにトークンを保管するとともに、その後のforward先
(その後に表示される)のWebページにも同じトークンを(hiddenパラメーターの形で)
組み込みます(Strutsが自動的に組み込みます)。

そのWebページのボタンなどをクリックして起動したActionクラスでは、(2)のメソッド
を呼び出すことによってWebブラウザー(Webページ)から(hiddenパラメーターの形で)
送られてきたトークンをセッション・オブジェクト内のトークンと照合し、一致して
いたらトランザクションを実行します。

また、同時に、トークンが一致していた場合には(3)のメソッドを呼び出すことによって、
セッションに保管していたトークンを無効にします。
すると、ボタンの二度押しなどによって再度Webブラウザーから同じトークンが送られて
きても、一致するトークンがセッション・オブジェクト内に存在しないので、(2)のトー
クンの照合は失敗します。

照合に失敗したら、トランザクションの実行を回避します。


なお、(2)の代わりに

boolean isTokenValid(HttpServletRequest request, boolean reset)

というメソッドも用意されており、このメソッドの第二引数にtrueを指定すると、
トークンの照合後にトークンのリセットも行ってくれます。つまり、(2)と(3)を
両方実行してくれるようなメソッドになっています。こちらのほうが便利ですから、
通常はこのメソッドを使用します。


(これらのメソッドの詳細を知りたい人は、

http://www.jajakarta.org/struts/struts1.2/documentation/ja/target/api/org/apache/struts/action/Action.html

などを参照して下さい。なお、このWebページはエンコーディングがShift_JISですので、
文字化けする場合は、Webブラウザー(Internet Explorer)のメニュー・バーより
「表示」→「エンコード」→「日本語(シフトJIS)」を選択して下さい。)



◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆


では、実際にこれらをプログラミングしてみましょう。


まず、ConfirmOrderActionクラスを以下の通り編集しましょう。

(1) プロジェクト・エクスプローラーのStrutsShop配下の「Javaリソース: src」配下
のjp.co.flsi.lecture.struts(パッケージ名)配下のConfirmOrderAction.javaを
ダブルクリックします。

(2) ConfirmOrderAction.javaのエディターが開いたら、下記のように編集
し、保管(Ctrl+S)しましょう。

--------------------------------------------------------
package jp.co.flsi.lecture.struts;

import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Vector;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import jp.co.flsi.lecture.reflect.ValueLogStringMaker;
import jp.co.flsi.lecture.security.PasswordEncryption;
import jp.co.flsi.lecture.struts.db.Order;
import jp.co.flsi.lecture.struts.db.OrderItem;
import jp.co.flsi.lecture.struts.db.StruShopDbException;
import jp.co.flsi.lecture.struts.db.User;
import jp.co.flsi.lecture.struts.db.UserDbManager;

import org.apache.log4j.Logger;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

public class ConfirmOrderAction extends Action {
   private static Logger logger = Logger.getLogger(ConfirmOrderAction.class);

   public ActionForward execute(ActionMapping mapping, ActionForm form,
         HttpServletRequest request, HttpServletResponse response) {
      logger.info("Start ...............");
      UserDbManager userDbManager = new UserDbManager();
      try {
         ValueLogStringMaker stringMaker = new ValueLogStringMaker();
         logger.info("Method parameter: <<<<<" + stringMaker.getValues("mapping", mapping));
         logger.info(">>>>>");
         logger.info("Method parameter: <<<<<" + stringMaker.getValues("form", form));
         logger.info(">>>>>");
         logger.info("Method parameter: <<<<<" + stringMaker.getValues("request", request));
         logger.info(">>>>>");
         logger.info("Method parameter: <<<<<" + stringMaker.getValues("response", response));
         logger.info(">>>>>");
         saveToken(request);
         HttpSession session = request.getSession();
         Order anOrder = (Order)session.getAttribute("ORDER");
         if (anOrder == null) {
            logger.info("Method return: <<<<< cartVacant (1)");
            logger.info(">>>>>");
            return mapping.findForward("cartVacant");
         }
         Vector<OrderItem> orderItemList = anOrder.getOrderItems();
         if (orderItemList == null) {
            logger.info("Method return: <<<<< cartVacant (2)");
            logger.info(">>>>>");
            return mapping.findForward("cartVacant");
         }
         OrderForm orderForm = (OrderForm) form;
         anOrder.setPayMethod(Integer.parseInt(orderForm.getPayMethod()));
         // 新規に会員登録する場合:
         if (orderForm.getMembership().equals("new")) {
            logger.info("Method return: <<<<< newUser");
            logger.info(">>>>>");
            return mapping.findForward("newUser");
         }
         User user = null;
         userDbManager.connect();
         // 既に会員登録済みの場合:
         if (orderForm.getMembership().equals("yes")) {
            user = userDbManager.getDataByUserTypeAndUserid("R", orderForm.getUserid());
            //         (会員登録した顧客のユーザー・タイプは"R")
            // 入力されたユーザーIDに該当するユーザーがデータベースに見つからない場合:
            if (user.getUserid().equals("")) {
               logger.info("Method return: <<<<< useridInvalid");
               logger.info(">>>>>");
               return mapping.findForward("useridInvalid");
            }
            // 入力されたパスワードが正しくない場合:
            if (!PasswordEncryption.checkWithEncryptedPassword(orderForm.getPassword(), user.getPassword())) {
               logger.info("Method return: <<<<< passwordInvalid");
               logger.info(">>>>>");
               return mapping.findForward("passwordInvalid");
            }
         }
         else { // 会員登録せずに購入する場合:
            user = new User();
            String maxUserid = userDbManager.getMaxTemporaryUserid();
            user.setUserType("T");       // 一時的顧客のユーザー・タイプは"T"
            NumberFormat formatter = new DecimalFormat("0000000");
            user.setUserid(formatter.format(Integer.parseInt(maxUserid) + 1));
            user.setPassword(PasswordEncryption.encrypt("NONE"));
            user.setName(orderForm.getName());
            user.setZipcode(orderForm.getZipcode());
            user.setAddress(orderForm.getAddress());
            user.setTelno(orderForm.getTelNo());
            user.setEmail(orderForm.getEmailAddress());
            if (!userDbManager.insertData(user)) {
               logger.info("Method return: <<<<< userInsertError");
               logger.info(">>>>>");
               return mapping.findForward("userInsertError");
            }
         }
         anOrder.setUserType(user.getUserType());
         anOrder.setUserId(user.getUserid());
         session.setAttribute("USER", user);
      } catch (StruShopDbException e) {
         logger.error(e, e);
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         try {
            userDbManager.disconnect();
         }
         catch (Exception e) {
            logger.info("Disconnect failed or there is no connection.");
         }
         logger.info("End ...............");
      }
      logger.info("Method return: <<<<< success");
      logger.info(">>>>>");
      return mapping.findForward("success");
   }

}
--------------------------------------------------------

たんに42行目に

saveToken(request);

という行を追加しただけですね。



◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆


このConfirmOrderActionクラスが実行されたあと、confirmOrder.jspのWebページ
が表示され、そのWebページの「購入(注文確定)する」ボタンをクリックすると
CompleteOrderActionが実行されるわけですが、このCompleteOrderActionクラスを
下記のように編集しましょう。

(1) プロジェクト・エクスプローラーのStrutsShop配下の「Javaリソース: src」配下
のjp.co.flsi.lecture.struts(パッケージ名)配下のCompleteOrderAction.javaを
ダブルクリックします。

(2) CompleteOrderAction.javaのエディターが開いたら、下記のように編集
し、保管(Ctrl+S)しましょう。

--------------------------------------------------------
package jp.co.flsi.lecture.struts;

import java.util.Vector;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import jp.co.flsi.lecture.reflect.ValueLogStringMaker;
import jp.co.flsi.lecture.struts.db.Order;
import jp.co.flsi.lecture.struts.db.OrderDbManager;
import jp.co.flsi.lecture.struts.db.OrderItem;
import jp.co.flsi.lecture.struts.db.StruShopDbException;
import jp.co.flsi.lecture.struts.db.User;

import org.apache.log4j.Logger;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

public class CompleteOrderAction extends Action {
   private static Logger logger = Logger.getLogger(CompleteOrderAction.class);

   public ActionForward execute(ActionMapping mapping, ActionForm form,
         HttpServletRequest request, HttpServletResponse response) {
      logger.info("Start ...............");
      if (isTokenValid(request, true)) {
         OrderDbManager orderDb = new OrderDbManager();
         try {
            ValueLogStringMaker stringMaker = new ValueLogStringMaker();
            logger.info("Method parameter: <<<<<" + stringMaker.getValues("mapping", mapping));
            logger.info(">>>>>");
            logger.info("Method parameter: <<<<<" + stringMaker.getValues("form", form));
            logger.info(">>>>>");
            logger.info("Method parameter: <<<<<" + stringMaker.getValues("request", request));
            logger.info(">>>>>");
            logger.info("Method parameter: <<<<<" + stringMaker.getValues("response", response));
            logger.info(">>>>>");
            HttpSession session = request.getSession();
            Order anOrder = (Order)session.getAttribute("ORDER");
            if (anOrder == null) {
               logger.info("Method return: <<<<< cartVacant (1)");
               logger.info(">>>>>");
               return mapping.findForward("cartVacant");
            }
            Vector<OrderItem> orderItemList = anOrder.getOrderItems();
            if (orderItemList == null) {
               logger.info("Method return: <<<<< cartVacant (2)");
               logger.info(">>>>>");
               return mapping.findForward("cartVacant");
            }
            User user = (User)session.getAttribute("USER");
            if (user == null) {
               logger.info("Method return: <<<<< userVacant");
               logger.info(">>>>>");
               return mapping.findForward("userVacant");
            }
            orderDb.connect();
            orderDb.insertData(anOrder);
         } catch (StruShopDbException e) {
            logger.error(e, e);
         }
         catch (Throwable e) {
            logger.error(e, e);
         }
         finally {
            try {
               orderDb.disconnect();
            }
            catch (Exception e) {
               logger.info("Disconnect failed or there is no connection.");
            }
            logger.info("End ...............");
         }
         logger.info("Method return: <<<<< success");
         logger.info(">>>>>");
         return mapping.findForward("success");
      }
      else {
         return mapping.findForward("tokenInvalid");
      }
   }

}
--------------------------------------------------------

たんに、全体を

if (isTokenValid(request, true)) {
    *
    *
    *
}
else {
   return mapping.findForward("tokenInvalid");
}

で囲んだだけですね。
つまり、Webページ側のトークンとセッションに保管されているトークンが
一致しなかった場合、あるいはセッションに保管されていたトークンがリセット
されていたために照合に失敗した場合、注文のトランザクション(注文に関する
一連の処理)は実行せずにforwardに"tokenInvalid"という値を設定するように
しています。
(トークンが一致した場合は、いままで通り注文のトランザクションが実行され
ます。)



◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆


では、forwardの値が"tokenInvalid"だったときにtokenInvalid.jspという
JSPファイルのWebページを表示するようにstruts-config.xmlに登録して
おきましょう。

(1) プロジェクト・エクスプローラーのStrutsShop配下のWebContent配下の
WEB-INF配下のstruts-config.xmlをダブル・クリックします。
(あるいは、フロー・エディターを確実に開くためには、WEB-INF配下の
struts-config.xmlを右クリック→「アプリケーションから開く」→
「struts-config.xmlエディター」を選択します。)

(2) struts-config.xmlのフロー・エディターが開いたら
「struts-config.xml」タブをダブル・クリックしてエディターを最大化して
おきましょう。(元に戻すには再度タブをダブル・クリック)

(3) フロー・エディターの左側に並んでいるアイコンのうち、「ページ」を
クリックし、/conmpletemorderのアイコンの左下のどこか空いている白い領域
をクリックしましょう。

(4) 貼り付いたアイコンの文字列を

/tokenInvalid.jsp

に書き換えて下さい。(文字列を書き換えるには文字列を2回クリック
(ダブル・クリックではない。1回目のクリックで選択、2回目のクリック
で文字列の変更の意味になる)します。)

(5) 続いて、左側に並んでいるアイコンのうち、「進む」をクリックし、
/conmpletemorderのアイコンをクリックし、次に、先ほど貼り付けた
/tokenInvalid.jspのアイコンをクリックします。
そうすると、2つのアイコンの間に矢印の線が引かれますね。

このままだと、矢印線を引くモードのままになっているので、いったん「選択」
というアイコンをクリックすることによってモードを解除しておいて下さい。

(6) 先ほどの矢印線に「forward1」というような文字列が付いていると思い
ますが、これを

tokenInvalid

に書き換えて下さい。

(7) Ctrl + Sキーでファイルを保管しておきましょう。



◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆


このままだと、/tokenInvalid.jspのアイコンに警告のマーク(丸みを帯びた
橙色の小さな三角形の中にエクスクラメーション・マーク(ビックリ・マーク)
が入ったアイコン)が付いていますが、これはこれらのファイルが実在しない
からなので、これからそれを作成しましょう。

(1) /tokenInvalid.jspのアイコンをダブル・クリックします。

(2) 「Struts JSPファイル」ウインドウが表示されたら、「完了」ボタンを
クリックします。

(3) tokenInvalid.jspのエディターが開いたら、下記のように編集して保管しま
しょう。

--------------------------------------------------------
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="/tags/struts-bean" prefix="bean" %>
<%@ taglib uri="/tags/struts-logic" prefix="logic" %>
<%@ taglib uri="/tags/struts-html" prefix="html" %>
<%@ taglib uri="/tags/struts-nested" prefix="nested" %>

<html:html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
      <title>ボタン二度押しエラー</title>
   </head>
   <body bgcolor="#77ffff" text="#fa5a00">
      <h1>ボタン二度押しエラー</h1>
      <font color="#ff0000">ボタンが重複して二度押されました。1回目の押下だけが有効になります。</font>
      <br>
      <br>
      <html:link page="/itemSelect.jsp">トップ・ページ(商品検索のページ)に戻る</html:link>
   </body>
</html:html>
--------------------------------------------------------



◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆


では、動作確認してみましょう。

(1) 「サーバー」ビューの中の「ローカル・ホストのTomcat v5.5サーバー」を
右クリックし、「開始」を選択します。しばらくして、

ローカル・ホストのTomcat v5.5サーバー[始動済み,同期済み]

というように、後ろに「始動済み」の表示が出たら、Tomcatの起動が完了しています。

(2) Webブラウザー(Internet Explorer)を起動して、URL

http://localhost:8080/StrutsShop/itemSelect.jsp

を入力しましょう。

(3) 「商品の検索」のWebページが開いたら、そのまま「商品検索」ボタンをクリック
して、次の「商品のリスト」のWebページでは全ての選択欄にチェックマークを入れて
「買物かごに入れる」ボタンをクリックしましょう。

(4) 「ショッピング・カートの中のリスト」のWebページが開いたら、そのまま
「これらの商品を購入する」ボタンをクリックしましょう。

(5) 「購入する商品」のWebページが開いたら、「お支払い方法」欄は
「クレジット・カード払い」のままにし、「会員登録済み」を選択(その左側
の丸の中をクリック)し、「ユーザーID」欄には、以前会員登録した
user01
を入力し、「パスワード」欄には、以前登録した正しいパスワード
pass01
を入力し、「最終購入確認または新規会員登録の画面に進む」ボタンを
クリックしましょう。

(10) 購入(注文)内容の確認のWebページが開いたら、Webブラウザーのメニュー・バー
から「表示」→「ソース」を選択することによって、HTMLのソースを表示してみて
下さい。
この中に
<input type="hidden" name="org.apache.struts.taglib.html.TOKEN" value="......">
というようなコードがあるはずです。これがhiddenの形式でWebページに組み込まれた
トークンです。
このWebページの「購入(注文確定)する」ボタンをクリックすると、このhiddenパラメー
ターも他のパラメーターと同様にサーバーに送られることになります。
では、Webページのほうに戻って、「購入(注文確定)する」ボタンをクリックして
下さい。(HTMLのソースは閉じてよい。)

(11) 注文完了のWebページ(「下記のご注文を受け付けました。」のWebページ)が
正常に表示されることを確認して下さい。

(12) では、このWebページ上の「トップ・ページ(商品検索のページ)に戻る」を
クリックして、再度「商品の検索」のWebページに戻ることにしましょう。

(3) 「商品の検索」のWebページが開いたら、そのまま「商品検索」ボタンをクリック
して、次の「商品のリスト」のWebページでは全ての選択欄にチェックマークを入れて
「買物かごに入れる」ボタンをクリックしましょう。

(4) 「ショッピング・カートの中のリスト」のWebページが開いたら、そのまま
「これらの商品を購入する」ボタンをクリックしましょう。

(5) 「購入する商品」のWebページが開いたら、「お支払い方法」欄は
「クレジット・カード払い」のままにし、「会員登録済み」を選択(その左側
の丸の中をクリック)し、「ユーザーID」欄には、以前会員登録した
user01
を入力し、「パスワード」欄には、以前登録した正しいパスワード
pass01
を入力し、「最終購入確認または新規会員登録の画面に進む」ボタンを
クリックしましょう。

(10) 購入(注文)内容の確認のWebページが開いたら、「購入(注文確定)する」ボタンを
今度は、連続して2〜3回クリックしてみましょう。

「ボタン二度押しエラー」のページが開きますね。(PCの動作が滅茶苦茶速い場合は、
二度押しするよりも速く注文完了のWebページが返ってきて、「ボタン二度押しエラー」
のWebページが表示されないかもしれませんが、その場合は動作確認はあきらめて
下さい。)

注文処理が正しく行われていることを確認するために、EclipseのDBViewerで
ORDERHEADERテーブルとORDERITEMテーブルを表示して、正しくデータが登録されて
いることを確認して下さい。

(DBViewerの操作方法を忘れた人は、vol.084などを復習のこと。)



なお、今回作成したtokenInvalid.jspファイルでは、「ボタン二度押しエラー」
が報告されるだけで、注文が完了したかどうかの報告はされませんので、
別途データベースを調べて注文が完了したデータを表示するなどのプログラミング
をしておくことが望ましいですが、そこらへんの処理は省略いたします。



◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆


(次回に続く)



┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
★ホームページ:
      http://www.flsi.co.jp/Java_text/
★このメールマガジンは
     「まぐまぐ(http://www.mag2.com)」
 を利用して発行しています。
★バックナンバーは
      http://www.flsi.co.jp/Java_text/
 にあります。
★このメールマガジンの登録/解除は下記Webページでできます。
      http://www.mag2.com/m/0000193915.html
★このメールマガジンへの質問は下記Webページにて受け付けて
 います。わからない所がありましたら、どしどしと質問をお寄
 せください。
      http://www.flsi.co.jp/Java_text/
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Copyright (C) 2010 Future Lifestyle Inc. 不許無断複製