■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
                      2009年06月14日

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

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


[このメールマガジンは、画面を最大化して見てください。]


========================================================
◆ 01.SOAPのアプリケーション(Webサービス)
========================================================


では、ソース・コードを修正していきましょう。

修正の仕方は、まずDbManagerのconnフィールドのstatic指定を取り除いて保管すれ
ば、(Eclipseが)DbManagerおよびそのサブクラスのメソッドの中でエラー(コンパイル・
エラー)を知らせてくれます(赤い×マークがつく)から、それらのメソッドからも
static指定を取り除きます。
(staticメソッドはクラス・メソッドであるためインスタンス・フィールド(static
でないフィールド)にアクセスすることはできません。そのため上記のようにメソッド
の中にコンパイル・エラーが発生したのです。これらのエラーはインスタンス・メソッド
に変更する(つまりstatic指定を取り除く)だけで無くなります。)

これらを保管すると、今度はHotelSoapBindingImplのほうにエラー(コンパイル・
エラー)が表示されますね。DbManagerとそのサブクラスのメソッドがすべてインスタ
ンス・メソッドに変わってしまったので、インスタンス生成しておかないとメソッド
を呼び出せないからです。
というわけで、予めインスタンス生成を行っておいて、あとはインスタンスのメソッ
ドを呼び出すようにソース・コードを書き換えていきます。


このとき、DbManagerのstaticフィールドをインスタンス・フィールドに変更したこと
によるサブクラスへの影響について、もう一つ注意しないといけないことがあります。

DbManager(スーパークラス)のconnフィールドはprotected指定になっていますが、
このフィールドがstaticフィールドであったときは、サブクラスのstaticメソッド
からも自分のフィールドとしてアクセスできていました。スーパークラスのstatic
フィールドは、それをサブクラスに継承している場合(protected指定やpublic指定
によってサブクラスでもアクセスできるようになっている場合)はサブクラスにも
共有されます。
したがって、DbManagerのconnect()メソッドを実行することによって、connフィールド
にConnectionオブジェクトを入れておけば、それをサブクラス(たとえばRoomInfoDbManager)
でもそのまま自分のフィールドとして使用できました。

ところが、static指定を取り除いてインスタンス・フィールドに変えてしまうと、
これはインスタンスごとに別々に用意される変数になります。

そうすると、DbManagerのインスタンスを生成してそのconnect()メソッドを実行しても、
そのconnフィールドに代入されるConnectionオブジェクトは、そのDbManagerのインスタ
ンスだけのものになってしまいます。
たとえ、サブクラス(たとえばRoomInfoDbManager)がこのconnフィールドを継承して
いても(実際、DbManagerのconnフィールドはprotected指定なのでサブクラスに継承
されている)サブクラスのインスタンスが持つconnフィールドとは別のものです。

したがって、DbManagerのインスタンスのconnect()メソッドを実行しても、サブクラス
(たとえばRoomInfoDbManager)のインスタンスのconnフィールドには何も影響しない
のです。

したがって、たとえばRoomInfoDbManagerのconnフィールドにConnectionオブジェクト
を代入したいときはDbManagerではなくRoomInfoDbManagerのインスタンス(がDbManager
から継承している)のconnect()メソッドを実行する必要があります。


HotelSoapBindingImplのfindRooms()メソッドのソース・コードを見てみると、
もう一つ面倒なことがあります。その中で呼び出されているDbManagerのサブクラス
はRoomInfoDbManagerとRoomBookDbManagerの2つあるのです。
それぞれのインスタンスのconnフィールドにConnectionオブジェクトを代入するとなる
と、重複して2つのデータベース接続を行うことになります。
本来なら1つのデータベース接続を通して順番に複数のテーブルにアクセスすれば
済むものを、わざわざ2つのデータベース接続を使って行うのは無駄です。

そもそもRoomInfoDbManagerのメソッドとRoomBookDbManagerのメソッドは互いに連係
して用いられています。
したがって、これらのメソッドは一つのクラスにまとめてしまったほうがすっきり
するし、データベース接続も一つで済むので処理が楽になります。


というわけで、RoomInfoDbManagerとRoomBookDbManagerをまとめて一つのクラスとして、
下記のようなRoomDbManagerというクラスを作りましょう。
(jp.co.flsi.lecture.webservice.hotel.dbパッケージの中に作ります。)

--------------------------------------------------------
package jp.co.flsi.lecture.webservice.hotel.db;

import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.GregorianCalendar;
import java.util.Vector;

import org.apache.log4j.Logger;

import jp.co.flsi.lecture.webservice.hotel.RoomInfo;

public class RoomDbManager extends DbManager {
   private static final String selectAllSql = "SELECT * FROM ROOMINFO";
   private static final String selectCapaSql = "SELECT * FROM ROOMINFO WHERE CAPACITY > ?";
   private static final String selectCapaAndMinRateSql = "SELECT * FROM ROOMINFO WHERE CAPACITY > ? AND RATE > ?";
   private static final String selectCapaAndRateSql = "SELECT * FROM ROOMINFO WHERE CAPACITY > ? AND RATE > ? AND RATE < ?";
   private static final String selectRoomnumAndDateSql = "SELECT * FROM ROOMBOOK WHERE ROOMNUM = ? AND DATE = ?";
   private static Logger logger = Logger.getLogger(RoomDbManager.class);

   public boolean isBooked(int roomNum, GregorianCalendar stayDate) throws HotelDbException {
      logger.info("Start ...............");
      boolean booked = false;
      try {
         PreparedStatement selectPs = null;
         ResultSet rs;
         selectPs = conn.prepareStatement(selectRoomnumAndDateSql);
         selectPs.setInt(1, roomNum);
         selectPs.setDate(2, new Date(stayDate.getTimeInMillis()));
         rs = selectPs.executeQuery();
         while (rs.next()) {
            booked = true;
         }
         selectPs.close();
      }
      catch (SQLException e) {
         logger.error(e, e);
         throw new HotelDbException("Error: isBooked() failed!", e);
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         logger.info("End ...............");
      }
      return booked;
   }

   public Vector<RoomInfo> getAllData() throws HotelDbException {
      Vector<RoomInfo> roomInfoList = new Vector<RoomInfo>();
      logger.info("Start ...............");
      try {
         Statement selectSql = conn.createStatement();
         ResultSet rs = selectSql.executeQuery(selectAllSql);
         while (rs.next()) {
            RoomInfo roomInfo = new RoomInfo();
            roomInfo.setRoomNum(rs.getInt("ROOMNUM"));
            logger.info("ROOMNUM = " + rs.getInt("ROOMNUM"));
            roomInfo.setCapacity(rs.getInt("CAPACITY"));
            roomInfo.setRatePerNight(rs.getInt("RATE"));
            roomInfo.setRoomType(rs.getString("TYPE"));
            if ("Y".equals(rs.getString("WEB"))) {
               roomInfo.setWebBrowsable(true);
            }
            else {
               roomInfo.setWebBrowsable(false);
            }
            roomInfoList.add(roomInfo);
         }
         selectSql.close();
      }
      catch (SQLException e) {
         logger.error(e, e);
         throw new HotelDbException("Error: getAllData() failed!", e);
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         logger.info("End ...............");
      }
      return roomInfoList;
   }

   public Vector<RoomInfo> getData(int capacity, int minRate, int maxRate) throws HotelDbException {
      Vector<RoomInfo> roomInfoList = new Vector<RoomInfo>();
      logger.info("Start ...............");
      try {
         PreparedStatement selectPs = null;
         ResultSet rs;
         if (maxRate > -1) {
            selectPs = conn.prepareStatement(selectCapaAndRateSql);
            selectPs.setInt(1, capacity);
            selectPs.setInt(2, minRate);
            selectPs.setInt(3, maxRate);
         }
         else if (minRate > -1) {
            selectPs = conn.prepareStatement(selectCapaAndMinRateSql);
            selectPs.setInt(1, capacity);
            selectPs.setInt(2, minRate);
         }
         else {
            selectPs = conn.prepareStatement(selectCapaSql);
            selectPs.setInt(1, capacity);
         }
         rs = selectPs.executeQuery();
         while (rs.next()) {
            RoomInfo roomInfo = new RoomInfo();
            roomInfo.setRoomNum(rs.getInt("ROOMNUM"));
            logger.info("ROOMNUM = " + rs.getInt("ROOMNUM"));
            roomInfo.setCapacity(rs.getInt("CAPACITY"));
            roomInfo.setRatePerNight(rs.getInt("RATE"));
            roomInfo.setRoomType(rs.getString("TYPE"));
            if ("Y".equals(rs.getString("WEB"))) {
               roomInfo.setWebBrowsable(true);
            }
            else {
               roomInfo.setWebBrowsable(false);
            }
            roomInfoList.add(roomInfo);
         }
         selectPs.close();
      }
      catch (SQLException e) {
         logger.error(e, e);
         throw new HotelDbException("Error: getData() failed!", e);
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         logger.info("End ...............");
      }
      return roomInfoList;
   }

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

なお、このクラスを楽に作成するためには、

(1) Eclipseのパッケージ・エクスプローラーにおいてRoomInfoDbManagerを右クリック
して「コピー」を選択。

(2) 同じくパッケージ・エクスプローラーにおいて、
jp.co.flsi.lecture.webservice.hotel.dbパッケージを右クリックし、「貼り付け」
を選択。

(3) 「名前の競合」ウインドウにおいて、
RoomDbManager
と入力し、「OK」ボタンをクリック。

(4) RoomDbManagerのソース・コードを開いて、RoomBookDbManagerの中身(メソッドと
selectRoomnumAndDateSqlフィールド)をコピーして貼り付ける。
(リファクタリングのプルアップとプッシュダウンを使えばメソッドやフィールドを
スーパークラスやサブクラスに移動することができるので、これを使ってスーパークラス
経由で移動するという方法もある。)

というような手順で行うとよいでしょう。


あとは、いらなくなったRoomInfoDbManagerとRoomBookDbManagerを削除しておいて
ください。(パッケージ・エクスプローラーにおいてRoomInfoDbManagerと
RoomBookDbManagerをそれぞれ右クリック→「削除」を選択)


念のために、修正(staticを取り除いた)後のBookingDbManagerとDbManagerの
ソース・コードもそれぞれ下に提示しておきます。

[BookingDbManager]
--------------------------------------------------------
package jp.co.flsi.lecture.webservice.hotel.db;

import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Calendar;
import java.util.GregorianCalendar;

import org.apache.log4j.Logger;

public class BookingDbManager extends DbManager {
   private static final String selectMaxBooknumSql = "SELECT MAX(BOOKNUM) FROM BOOKING";
   private static final String insertBookingSql = "INSERT INTO BOOKING  VALUES (?, ?, ?, ?)";
   private static final String insertRoombookSql = "INSERT INTO ROOMBOOK  VALUES (?, ?, ?, ?)";
   private static Logger logger = Logger.getLogger(BookingDbManager.class);

   public void insertData(int roomNum, String customerName, String address, String telNum,
         GregorianCalendar startStayDate, int stayNights, int lodgers) throws HotelDbException {
      logger.info("Start ...............");
      try {
         Statement selectSt = conn.createStatement();
         PreparedStatement insertBookingPs = conn.prepareStatement(insertBookingSql);
         PreparedStatement insertRoombookPs = conn.prepareStatement(insertRoombookSql);
         ResultSet rs = selectSt.executeQuery(selectMaxBooknumSql);
         int newBookingNumber = 1;
         if (rs.next()) newBookingNumber = rs.getInt(1) + 1;
         insertBookingPs.setInt(1, newBookingNumber);
         insertBookingPs.setString(2, customerName);
         insertBookingPs.setString(3, address);
         insertBookingPs.setString(4, telNum);
         insertBookingPs.executeUpdate();
         GregorianCalendar stayDate = (GregorianCalendar) startStayDate.clone();
         for (int i = 0; i < stayNights; i++) {
            insertRoombookPs.setInt(1, roomNum);
            insertRoombookPs.setDate(2, new Date(stayDate.getTimeInMillis()));
            insertRoombookPs.setInt(3, newBookingNumber);
            insertRoombookPs.setInt(4, lodgers);
            insertRoombookPs.executeUpdate();
            stayDate.add(Calendar.DAY_OF_MONTH, 1);
         }
         selectSt.close();
         insertBookingPs.close();
         insertRoombookPs.close();
         conn.commit();
      }
      catch (SQLException e) {
         logger.error(e, e);
         throw new HotelDbException("Error: insertData() failed!", e);
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         logger.info("End ...............");
      }
   }
  
}
--------------------------------------------------------


[DbManager]
--------------------------------------------------------
package jp.co.flsi.lecture.webservice.hotel.db;

import java.sql.SQLException;
import java.sql.Connection;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

import org.apache.log4j.Logger;

public class DbManager {
   protected Connection conn = null;
   private static Logger logger = Logger.getLogger(DbManager.class);

   public void connect() throws HotelDbException {
      logger.info("Start ...............");
      try{
         if (conn == null || conn.isClosed()) {
            Context initCtx = new InitialContext();
            if(initCtx == null) throw new HotelDbException("Error: InitialContext could not be generated!");
            DataSource ds = (DataSource) initCtx.lookup("java:comp/env/jdbc/HOTELDB");
            if (ds != null) {
               conn = ds.getConnection();
               if(conn == null) throw new HotelDbException("Error: Connection does not exist!");
               else logger.info("Connection has been gotten.");
            }
            else {
               throw new HotelDbException("Error: DataSource does not exist!");
            }
         }
      } catch (NamingException e) {
         logger.error(e, e);
         throw new HotelDbException("Error: Connect() failed!", e);
      } catch (SQLException e) {
         logger.error(e, e);
         throw new HotelDbException("Error: Connect() failed!", e);
      } catch (HotelDbException e) {
         logger.error(e, e);
         throw new HotelDbException("Error: Connect() failed!", e);
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         logger.info("End ...............");
      }
   }

   public void disconnect() {
      logger.info("Start ...............");
      try {
         conn.close();
      } catch(SQLException e) {
         logger.error(e, e);
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         logger.info("End ...............");
      }
   }

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


あとはHotelSoapBindingImplのソース・コードにおいて、DbManagerのサブクラスの
インスタンス生成を行って、そのインスタンスのメソッドを呼び出すようにソース・
コードを修正すれば完成ですね。
修正する場所は、やはりHotelSoapBindingImplのコンパイル・エラーになっている
ところを目印にすればすぐにわかります。
修正例を下に提示します。

--------------------------------------------------------
/**
 * HotelSoapBindingImpl.java
 *
 * このファイルはWSDLから自動生成されました / [en]-(This file was auto-generated from WSDL)
 * Apache Axis 1.4 Apr 22, 2006 (06:55:48 PDT) WSDL2Java生成器によって / [en]-(by the Apache Axis 1.4 Apr 22, 2006 (06:55:48 PDT) WSDL2Java emitter.)
 */

package jp.co.flsi.lecture.webservice.hotel;

import java.rmi.RemoteException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Vector;

import jp.co.flsi.lecture.webservice.hotel.db.BookingDbManager;
import jp.co.flsi.lecture.webservice.hotel.db.HotelDbException;
import jp.co.flsi.lecture.webservice.hotel.db.RoomDbManager;

import org.apache.log4j.Logger;

public class HotelSoapBindingImpl implements Hotel{

   private static Logger logger = Logger.getLogger(HotelSoapBindingImpl.class);

   public RoomInfo[] findRooms(StayInfoInput stayInfo) throws RemoteException {
      logger.info("Start ...............");
      Vector<RoomInfo> roomInfoVector = null;
      RoomDbManager roomDbManager = new RoomDbManager();

      try {
         String startDate = stayInfo.getStartDate();
         if (startDate.length() != 8) {
            logger.info("Invalid parameter: startDate(= " + startDate
                  + ") format not correct: must be 8 letters");
            throw new RemoteException("Invalid parameter: startDate(= " + startDate
                  + ") is invalid: must be 8 letters");
         }
         SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
         GregorianCalendar stayDate = new GregorianCalendar();
         stayDate.setTime(dateFormat.parse(startDate));
         int numOfNights = stayInfo.getNumOfNights();
         int numOfLodgers = stayInfo.getNumOfLodgers();
         if (numOfNights < 1 || numOfLodgers < 1) {
            logger.info("Invalid parameter: numOfNights (" + numOfNights + ") or numOfLodgers ("
                  + numOfLodgers + ") is invalid.");
            throw new RemoteException("Invalid parameter: numOfNights (" + numOfNights
                  + ") or numOfLodgers (" + numOfLodgers + ") is invalid.");
         }

         roomDbManager.connect();
         roomInfoVector = roomDbManager.getData(numOfLodgers, stayInfo.getMinRatePerNight(),
               stayInfo.getMaxRatePerNight());
         int roomInfoSize = roomInfoVector.size();
         if (roomInfoSize > 0) {
            for (int roomIndex = roomInfoSize - 1; roomIndex > -1; roomIndex--) {
               RoomInfo roomInfo = roomInfoVector.get(roomIndex);
               int roomNum = roomInfo.getRoomNum();
               for (int i = 0; i < numOfNights; i++) {
                  if (roomDbManager.isBooked(roomNum, stayDate)) {
                     roomInfoVector.remove(roomIndex);
                     break;
                  }
                  stayDate.add(Calendar.DAY_OF_MONTH, 1);
               }
            }
         }
      } catch (HotelDbException e) {
         logger.error(e, e);
      } catch (ParseException e) {
         logger.info(e);
         throw new RemoteException("ParseException occured (startDate format not correct)", e);
      }
      catch (RemoteException e) {
         logger.info(e);
         throw e;
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         try {
            roomDbManager.disconnect();
         }
         catch (Exception e) {
            logger.info("Disconnect failed or there is no connection.");
         }
         logger.info("End ...............");
      }

      return roomInfoVector.toArray(new RoomInfo[roomInfoVector.size()]);
   }

   public boolean reserveRoom(RoomReserveInfo roomReserve) throws RemoteException {
      logger.info("Start ...............");
      BookingDbManager bookingDbManager = new BookingDbManager();

      try {
         String startDate = roomReserve.getStartDate();
         if (startDate.length() != 8) {
            logger.info("Invalid parameter: startDate(= " + startDate
                  + ") format not correct: must be 8 letters");
            throw new RemoteException("Invalid parameter: startDate(= " + startDate
                  + ") is invalid: must be 8 letters");
         }
         SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
         GregorianCalendar startStayDate = new GregorianCalendar();
         startStayDate.setTime(dateFormat.parse(startDate));
         int numOfNights = roomReserve.getNumOfNights();
         int numOfLodgers = roomReserve.getNumOfLodgers();
         if (numOfNights < 1 || numOfLodgers < 1) {
            logger.info("Invalid parameter: numOfNights (" + numOfNights + ") or numOfLodgers ("
                  + numOfLodgers + ") is invalid.");
            throw new RemoteException("Invalid parameter: numOfNights (" + numOfNights
                  + ") or numOfLodgers (" + numOfLodgers + ") is invalid.");
         }
         bookingDbManager.connect();
         bookingDbManager.insertData(roomReserve.getRoomNum(), roomReserve.getName(),
               roomReserve.getAddress(), roomReserve.getTelNo(),
               startStayDate, numOfNights, numOfLodgers);
      } catch (HotelDbException e) {
         logger.error(e, e);
      } catch (ParseException e) {
         logger.info(e);
         throw new RemoteException("ParseException occured (startDate format not correct)", e);
      }
      catch (RemoteException e) {
         logger.info(e);
         throw e;
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         try {
            bookingDbManager.disconnect();
         }
         catch (Exception e) {
            logger.info("Disconnect failed or there is no connection.");
         }
         logger.info("End ...............");
      }
      return true;
   }

}

--------------------------------------------------------

なお、上記のソース・コードでは、以前

      catch (RemoteException e) {
         logger.info(e);
         throw new RemoteException(e.getMessage());
      }

としていたところを

      catch (RemoteException e) {
         logger.info(e);
         throw e;
      }

に書き直しています。
もうお分かりかと思いますが、ここではcatchする例外とそれをthrowするときの例外
の型が同じですから、newで作り直す必要はありません。うっかりして無駄なコードを
書いていたので、ついでに書き直しておきました。


では、修正が終わったら、例によってAntでデプロイしておきましょう。

デプロイが終わったら、例によってHotelClientを実行してテストしてみましょう。
正常に終了しますね。



さて、以前(vol.152で)SimpleDateFormatを使ってYYYYMMDD形式の日付をDateオブ
ジェクトに変換する方法を紹介しましたが、そのとき、日付の形式のチェックとしては
まだ完璧ではない、ということを書きました。

これは、たとえば、20090633などというありえない日付(2009年6月33日なんて日付は
ありえない)を指定しても、例外(ParseException)が発生することなく、正常に
Dateオブジェクトに変換されてしまうのです。
実際には、この日付は(6月の最後の日が30日であることから、それより3日あとの日だ
と解釈されて)2009年7月3日と解釈されてDateオブジェクトに変換されてしまいます。

このように日付が拡大解釈されてしまう仕様になっているため、ParseExceptionをcatch
するだけでは、日付の形式の完璧なチェックにはならないのです。

しかし、こういう場合でも、Dateオブジェクトに変換(さらにGregorianCalendarオブジェ
クトに変換)した後の年・月・日を元のYYYYMMDD形式の日付の年・月・日と一致するか
どうかを確認することによって、簡単に完璧な日付の形式のチェックが行えます。

具体的には、たとえばHotelSoapBindingImplの

stayDate.setTime(dateFormat.parse(startDate));

の行の下に

if (stayDate.get(Calendar.YEAR) != Integer.parseInt(startDate.substring(0, 4)) ||
      (stayDate.get(Calendar.MONTH) + 1) != Integer.parseInt(startDate.substring(4, 6)) ||
      stayDate.get(Calendar.DAY_OF_MONTH) != Integer.parseInt(startDate.substring(6))) {
   throw new RemoteException("Invalid parameter: startDate(= " + startDate
         + ") is not correct: impossible date");
}

というようなコードを追加すればいいのです。

これを踏まえて、HotelSoapBindingImplのソース・コードを下記のように変更しましょう。

--------------------------------------------------------
/**
 * HotelSoapBindingImpl.java
 *
 * このファイルはWSDLから自動生成されました / [en]-(This file was auto-generated from WSDL)
 * Apache Axis 1.4 Apr 22, 2006 (06:55:48 PDT) WSDL2Java生成器によって / [en]-(by the Apache Axis 1.4 Apr 22, 2006 (06:55:48 PDT) WSDL2Java emitter.)
 */

package jp.co.flsi.lecture.webservice.hotel;

import java.rmi.RemoteException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Vector;

import jp.co.flsi.lecture.webservice.hotel.db.BookingDbManager;
import jp.co.flsi.lecture.webservice.hotel.db.HotelDbException;
import jp.co.flsi.lecture.webservice.hotel.db.RoomDbManager;

import org.apache.log4j.Logger;

public class HotelSoapBindingImpl implements Hotel{

   private static Logger logger = Logger.getLogger(HotelSoapBindingImpl.class);

   public RoomInfo[] findRooms(StayInfoInput stayInfo) throws RemoteException {
      logger.info("Start ...............");
      Vector<RoomInfo> roomInfoVector = null;
      RoomDbManager roomDbManager = new RoomDbManager();

      try {
         String startDate = stayInfo.getStartDate();
         if (startDate.length() != 8) {
            logger.info("Invalid parameter: startDate(= " + startDate
                  + ") format not correct: must be 8 letters");
            throw new RemoteException("Invalid parameter: startDate(= " + startDate
                  + ") is invalid: must be 8 letters");
         }
         SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
         GregorianCalendar stayDate = new GregorianCalendar();
         stayDate.setTime(dateFormat.parse(startDate));
         if (stayDate.get(Calendar.YEAR) != Integer.parseInt(startDate.substring(0, 4)) ||
               (stayDate.get(Calendar.MONTH) + 1) != Integer.parseInt(startDate.substring(4, 6)) ||
               stayDate.get(Calendar.DAY_OF_MONTH) != Integer.parseInt(startDate.substring(6))) {
            logger.info("Invalid parameter: startDate(= " + startDate
                  + ") is not correct: impossible date");
            throw new RemoteException("Invalid parameter: startDate(= " + startDate
                  + ") is not correct: impossible date");
         }
         int numOfNights = stayInfo.getNumOfNights();
         int numOfLodgers = stayInfo.getNumOfLodgers();
         if (numOfNights < 1 || numOfLodgers < 1) {
            logger.info("Invalid parameter: numOfNights (" + numOfNights + ") or numOfLodgers ("
                  + numOfLodgers + ") is invalid.");
            throw new RemoteException("Invalid parameter: numOfNights (" + numOfNights
                  + ") or numOfLodgers (" + numOfLodgers + ") is invalid.");
         }

         roomDbManager.connect();
         roomInfoVector = roomDbManager.getData(numOfLodgers, stayInfo.getMinRatePerNight(),
               stayInfo.getMaxRatePerNight());
         int roomInfoSize = roomInfoVector.size();
         if (roomInfoSize > 0) {
            for (int roomIndex = roomInfoSize - 1; roomIndex > -1; roomIndex--) {
               RoomInfo roomInfo = roomInfoVector.get(roomIndex);
               int roomNum = roomInfo.getRoomNum();
               for (int i = 0; i < numOfNights; i++) {
                  if (roomDbManager.isBooked(roomNum, stayDate)) {
                     roomInfoVector.remove(roomIndex);
                     break;
                  }
                  stayDate.add(Calendar.DAY_OF_MONTH, 1);
               }
            }
         }
      } catch (HotelDbException e) {
         logger.error(e, e);
      } catch (ParseException e) {
         logger.info(e);
         throw new RemoteException("ParseException occured (startDate format not correct)", e);
      }
      catch (RemoteException e) {
         logger.info(e);
         throw e;
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         try {
            roomDbManager.disconnect();
         }
         catch (Exception e) {
            logger.info("Disconnect failed or there is no connection.");
         }
         logger.info("End ...............");
      }

      return roomInfoVector.toArray(new RoomInfo[roomInfoVector.size()]);
   }

   public boolean reserveRoom(RoomReserveInfo roomReserve) throws RemoteException {
      logger.info("Start ...............");
      BookingDbManager bookingDbManager = new BookingDbManager();

      try {
         String startDate = roomReserve.getStartDate();
         if (startDate.length() != 8) {
            logger.info("Invalid parameter: startDate(= " + startDate
                  + ") format not correct: must be 8 letters");
            throw new RemoteException("Invalid parameter: startDate(= " + startDate
                  + ") is invalid: must be 8 letters");
         }
         SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
         GregorianCalendar startStayDate = new GregorianCalendar();
         startStayDate.setTime(dateFormat.parse(startDate));
         if (startStayDate.get(Calendar.YEAR) != Integer.parseInt(startDate.substring(0, 4)) ||
               (startStayDate.get(Calendar.MONTH) + 1) != Integer.parseInt(startDate.substring(4, 6)) ||
               startStayDate.get(Calendar.DAY_OF_MONTH) != Integer.parseInt(startDate.substring(6))) {
            logger.info("Invalid parameter: startDate(= " + startDate
                  + ") is not correct: impossible date");
            throw new RemoteException("Invalid parameter: startDate(= " + startDate
                  + ") is not correct: impossible date");
         }
         int numOfNights = roomReserve.getNumOfNights();
         int numOfLodgers = roomReserve.getNumOfLodgers();
         if (numOfNights < 1 || numOfLodgers < 1) {
            logger.info("Invalid parameter: numOfNights (" + numOfNights + ") or numOfLodgers ("
                  + numOfLodgers + ") is invalid.");
            throw new RemoteException("Invalid parameter: numOfNights (" + numOfNights
                  + ") or numOfLodgers (" + numOfLodgers + ") is invalid.");
         }
         bookingDbManager.connect();
         bookingDbManager.insertData(roomReserve.getRoomNum(), roomReserve.getName(),
               roomReserve.getAddress(), roomReserve.getTelNo(),
               startStayDate, numOfNights, numOfLodgers);
      } catch (HotelDbException e) {
         logger.error(e, e);
      } catch (ParseException e) {
         logger.info(e);
         throw new RemoteException("ParseException occured (startDate format not correct)", e);
      }
      catch (RemoteException e) {
         logger.info(e);
         throw e;
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         try {
            bookingDbManager.disconnect();
         }
         catch (Exception e) {
            logger.info("Disconnect failed or there is no connection.");
         }
         logger.info("End ...............");
      }
      return true;
   }

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

変更が終わったら、Antでデプロイしたあと、HotelClientを実行してテストして
みましょう。正常に終了しますね。

では、次にHotelClientで指定している日付を、ありえない日付に変えてから
実行しなおしてみてください。
たとえば、

stayInfo.setStartDate("20091225");

の行を

stayInfo.setStartDate("20090230");

などに書き換えます。(2月30日なんて日は、ありえませんね。)

今度は、実行するとRemoteExceptionが返ってきますね。たとえばコンソールに

java.rmi.RemoteException: Invalid parameter: startDate(= 20090230) is not correct: impossible date

のようなエラーが表示されますね。
他にもいろいろな日付でテストしてみて下さい。

このやり方だと、うるう年さえもちゃんと計算されてチェックされますから、完璧
ですね。
(正規表現を使った方法ではこう簡単にはいきませんね。ただでさえ複雑な表現に
なってしまう上に、うるう年のチェックまでするのはまず無理でしょう。)


なお、この日付のチェックを事前条件とし(日付のチェックはクライアント側の義務
とし)、契約による設計(Design By Contract)に基づいて表明(assert)を使用
する方法については、もうお分かりのことと思いますので、ここでは説明を省略しま
す。(何のことか分からない人はvol.151あたりを復習のこと。)




さて、前回までにメソッドにsynchronizedを指定する方法を説明してきましたが、
もう一つの、オブジェクトにsynchronizedを指定する方法について説明する機会
を失していましたので、ここで補足的に説明しておきます。


これまで説明した、メソッドにsynchronizedを指定する方法では、そのメソッド
の実行中にずっとオブジェクトやクラスにロックがかかったままになってしまい
ますので、メソッドの実行時間が長い場合にはアプリケーションのパフォーマンス
を悪化させてしまう大きな原因になる可能性があります。

特に、メソッドの中に書かれている多量のコードのうち、ほんの数行だけがロックの
必要な部分であった場合、メソッドにsynchronizedを指定する方法は、非常に無駄を
していることになります。

このような場合のために用意されたのが、オブジェクトにsynchronizedを指定する方法
です。
これは、

--------------------------------------------------------
synchronized (オブジェクト) {
   ・
   ・
   ・
}
--------------------------------------------------------

のような形式でオブジェクトにsynchronizedを指定するもので、オブジェクトは
インスタンス生成したものであれば何でもかまいません。
こうしておくと、指定されたオブジェクトにロックをかけたあと、ブロック(の中身)
が実行され、ブロックの実行が終わると、ロックが解除されます。
また、オブジェクトにロックをかけようとしたときに既に他者によってロックが
かけられている場合は、ロックが解除されるまで、このブロックの実行が待たされる
ことになります。

このブロックをメソッドの中のロックが必要な部分だけにかけておけば、メソッド
全体にsynchronizedを指定しなくて済みます。


詳しくは、下の演習で確認してください。



(次回に続く)


では、今日はここまでにします。



======================================================
◆ 02.演習問題
======================================================

オブジェクトにsynchronizedを指定する方法を確認するために、以下のような
クラスを作成してみてください。

--------------------------------------------------------
import java.util.Vector;

public class SerialNumberList {
   private Vector<Integer> integerVector = new Vector<Integer>();

   public void generate(int n) {
      Vector<Integer> integerVector2 = new Vector<Integer>();
      for (int i = 1; i <= n; i++) {
         integerVector2.add(i);
      }
      integerVector = integerVector2;
   }

   public void print() {
      System.out.println("******** ここで何か別のことをする。 ***********");
      System.out.println("******** ここで何か別のことをする。 ***********");
      System.out.println("******** ここで何か別のことをする。 ***********");
      for (Integer integer : integerVector) {
         System.out.println(integer);
      }
      System.out.println("******** ここで何か別のことをする。 ***********");
      System.out.println("******** ここで何か別のことをする。 ***********");
      System.out.println("******** ここで何か別のことをする。 ***********");
   }
}
--------------------------------------------------------

--------------------------------------------------------
import java.util.Vector;

public class SynchronizedSerialNumberList1 {
   private Vector<Integer> integerVector = new Vector<Integer>();

   // 競合状態が発生しないようにsynchronizedを指定
   // (このSynchronizedSerialNumberList1のインスタンスがロックされる)
   public synchronized void generate(int n) {
      Vector<Integer> integerVector2 = new Vector<Integer>();
      for (int i = 1; i <= n; i++) {
         integerVector2.add(i);
      }
      integerVector = integerVector2;
   }

   // 競合状態が発生しないようにsynchronizedを指定
   // (このSynchronizedSerialNumberList1のインスタンスがロックされる)
   public synchronized void print() {
      System.out.println("******** ここで何か別のことをする。 ***********");
      System.out.println("******** ここで何か別のことをする。 ***********");
      System.out.println("******** ここで何か別のことをする。 ***********");
      for (Integer integer : integerVector) {
         System.out.println(integer);
      }
      System.out.println("******** ここで何か別のことをする。 ***********");
      System.out.println("******** ここで何か別のことをする。 ***********");
      System.out.println("******** ここで何か別のことをする。 ***********");
   }
}
--------------------------------------------------------

--------------------------------------------------------
import java.util.Vector;

public class SynchronizedSerialNumberList2 {
   private Vector<Integer> integerVector = new Vector<Integer>();

   // 競合状態が発生しないようにsynchronizedを指定
   // (このSynchronizedSerialNumberList2のインスタンスがロックされる)
   public synchronized void generate(int n) {
      Vector<Integer> integerVector2 = new Vector<Integer>();
      for (int i = 1; i <= n; i++) {
         integerVector2.add(i);
      }
      integerVector = integerVector2;
   }

   public void print() {
      System.out.println("******** ここで何か別のことをする。 ***********");
      System.out.println("******** ここで何か別のことをする。 ***********");
      System.out.println("******** ここで何か別のことをする。 ***********");
      // (this(このSynchronizedSerialNumberList2のインスタンス)がロックされる)
      synchronized (this) {
         for (Integer integer : integerVector) {
            System.out.println(integer);
         }
      }
      System.out.println("******** ここで何か別のことをする。 ***********");
      System.out.println("******** ここで何か別のことをする。 ***********");
      System.out.println("******** ここで何か別のことをする。 ***********");
   }
}
--------------------------------------------------------

--------------------------------------------------------
// 普通のメソッドの実行(スレッド・セーフでない)
public class SynchronizedVectorTest implements Runnable {
   SerialNumberList serialNumberList = new SerialNumberList();
  
   public void run() {
      for (int i = 1; i <= 20; i++) {
         serialNumberList.generate(i);
      }
      serialNumberList.print();
   }

   public static void main(String[] args) {
      SynchronizedVectorTest sTest = new SynchronizedVectorTest();
      Thread[] aThread = new Thread[20];
      for (int i = 0; i < 20; i++) {
         aThread[i] = new Thread(sTest);
         aThread[i].start();
      }
   }

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

--------------------------------------------------------
// スレッド・セーフなメソッドの実行
public class SynchronizedVectorTest1 implements Runnable {
   SynchronizedSerialNumberList1 serialNumberList = new SynchronizedSerialNumberList1();
  
   public void run() {
      for (int i = 1; i <= 20; i++) {
         serialNumberList.generate(i);
      }
      serialNumberList.print();
   }

   public static void main(String[] args) {
      SynchronizedVectorTest1 sTest = new SynchronizedVectorTest1();
      Thread[] aThread = new Thread[20];
      for (int i = 0; i < 20; i++) {
         aThread[i] = new Thread(sTest);
         aThread[i].start();
      }
   }

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

--------------------------------------------------------
// スレッド・セーフなメソッドの実行
public class SynchronizedVectorTest2 implements Runnable {
   SynchronizedSerialNumberList2 serialNumberList = new SynchronizedSerialNumberList2();
  
   public void run() {
      for (int i = 1; i <= 20; i++) {
         serialNumberList.generate(i);
      }
      serialNumberList.print();
   }

   public static void main(String[] args) {
      SynchronizedVectorTest2 sTest = new SynchronizedVectorTest2();
      Thread[] aThread = new Thread[20];
      for (int i = 0; i < 20; i++) {
         aThread[i] = new Thread(sTest);
         aThread[i].start();
      }
   }

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

これらのSynchronizedVectorTest, SynchronizedVectorTest1, SynchronizedVectorTest2
を実行して、出力結果を確認しsynchronizedの働きが予想通りかどうか確認してください。



======================================================
◆ 03.前回の演習問題の答
======================================================

実行する各クラスにコメントを書いておいたので、どのクラスで問題が発生するか
(どのクラスなら問題は発生しないか)は既に分かっていることと思います。
ただし、たとえ競合条件(競合状態)が発生していても、悪いタイミングはほんの
たまにしか発生しないので、注意深くかつ根気強く実行結果を確認する必要が
あります。



┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
★ホームページ:
      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) 2009 Future Lifestyle Inc. 不許無断複製