package org.postgresforest.tool.recovery;


import java.io.*;
import java.net.*;
import java.sql.*;
import java.util.Hashtable;
import java.util.ArrayList;
import org.postgresforest.tool.Logger;
import org.postgresforest.vm.gsc.TableInfo;

class CancelThread extends Thread {
  private int bepid;
  private int timeout;
  private String url;
  private String user;
  private String pass;
  private boolean enabled = true;

  CancelThread(int bepid, int timeout, String url, String user, String pass)
  {
      this.bepid   = bepid;
      this.timeout = timeout;
      this.url     = url;
      this.user    = user;
      this.pass    = pass;

      this.enabled = true;

	  Logger.debug(toString() + " has been created.");
  }

  public void run()
  {
      try {
		  Logger.debug("CancelThread is going to wait " + timeout + " secs.");
		  synchronized (this)
		  {
			  wait(timeout*1000);
		  }

          if ( enabled )
          {
              Logger.notice("Going to cancel the backend (" + bepid + ")");

              Connection con = DriverManager.getConnection(url, user, pass);

              Statement stmt = con.createStatement();
              stmt.executeQuery("SELECT pg_cancel_backend(" +  bepid + ")");
              stmt.close();

              con.close();
          }
		  else
		  {
              Logger.debug(toString() + " has been finished without a cancel command.");
		  }
      }
      catch (Exception e)
      {
		  this.enabled = false;

		  Logger.debug(toString() + ": " + e.getMessage());
      }
  }

  public synchronized void disable()
  {
	  Logger.debug(toString() + " disabled.");
      this.enabled = false;

	  synchronized (this)
	  {
		  notify();
	  }
  }

	public String toString()
	{
		return "CancelThread: pid=" + bepid + ", url=" + url;
	}
}

public class RecoveryManager {
	private Connection src_con;
	private String src_url;
	private String src_user;
	private String src_pass;

	private Connection dest_con;
	private Hashtable tableNames;
	private Hashtable tableOids;

	private Statement readStmt;
	private ResultSet logRecords;
	private int numberOfLogRecords;
	private int maxLogXid;
	private int minLogXid;

	private Connection[] gsc_con = null;
	private String[] gsc_urls = null;

	private String gsc_url  = null;
	private String gsc_user = null;
	private String gsc_pass = null;

	public static final int LOG_MINXID = 1;
	public static final int LOG_MAXXID = 0x7FFFFFFF;

	private Timestamp log_begin_time = null;
	private Timestamp log_end_time   = null;

	private Timestamp log_apply_begin_time = null;
	private Timestamp log_apply_end_time   = null;

	public static int timeout = 60;

	// FIXME: config id must be selectable.
	private String configId = "FOREST_DEFAULT_CONFIG";
	private int cache_refresh;

	private int be_pid = 0;

	/**
	 * コンストラクタ。複製元URL・ユーザ名・パスワードと、
	 * 複製先URL・ユーザ名・パスワードを受け取り、
	 * 両者へのコネクションを確立する。
	 * 各メンバ変数を初期化する。
	 *
	 * @param dest_url  複製先DBのJDBC接続用のURL
	 * @param dest_user 複製先DBのJDBC接続用のユーザ名
	 * @param dest_pass 複製先DBのJDBC接続用のパスワード
	 * @param src_url  複製元DBのJDBC接続用のURL
	 * @param src_user 複製元DBのJDBC接続用のユーザ名
	 * @param src_pass 複製元DBのJDBC接続用のパスワード
	 */
	public RecoveryManager()
		throws ClassNotFoundException, SocketException
	{
		this(null, null);

		Class.forName("org.postgresql.Driver");
	}

	public RecoveryManager(String dest_url, String dest_user, String dest_pass,
						   String src_url, String src_user, String src_pass)
		throws ClassNotFoundException, SQLException, SocketException
	{

		this(null, null);

		Class.forName("org.postgresql.Driver");

		createDatabaseConnections(dest_url, dest_user, dest_pass,
								  src_url, src_user, src_pass);

	}

	public void setTimeout(int timeoutTime)
	{
		this.timeout = timeoutTime;
	}

	public void createDatabaseConnections(String dest_url,
										  String dest_user,
										  String dest_pass,
										  String src_url,
										  String src_user,
										  String src_pass)
		throws ClassNotFoundException, SQLException, SocketException
	{
		Logger.debug(src_url);
		Logger.debug(dest_url);

		if ( dest_url!=null && dest_user!=null && dest_pass!=null &&
			 src_url!=null && src_user!=null && src_pass!=null )
		{
			this.dest_con
				= DriverManager.getConnection(dest_url, dest_user, dest_pass);

			this.src_con
				= DriverManager.getConnection(src_url, src_user, src_pass);
		}
		else
		{
			Logger.error("createDatabaseConnections: invalid arguments.");
		}

		if ( this.src_con!=null )
			this.src_con.setAutoCommit(false);
		if ( this.dest_con!=null )
			this.dest_con.setAutoCommit(false);

		{
			Statement stmt = this.src_con.createStatement();
			ResultSet rs = stmt.executeQuery("SELECT pg_backend_pid()");
			if ( rs!=null && rs.next() )
				be_pid = rs.getInt(1);
			
			rs.close();
			stmt.close();
		}

		this.src_url  = src_url;
		this.src_user = src_user;
		this.src_pass = src_pass;

		Logger.debug("Backend PID=" + be_pid);
	}

	/**
	 * コンストラクタ。複製元DBへのConnectionと、
	 * 複製先DBへのConnectionを受け取り、各メンバ変数を初期化する。
	 *
	 * @param dest_con  複製先DBへのConnectionオブジェクト
	 * @param src_con   複製元DBへのConnectionオブジェクト
	 */
	public RecoveryManager(Connection dest_con, Connection src_con)
		throws SocketException
	{
		tableNames = null;
		readStmt = null;
		logRecords = null;

		maxLogXid = minLogXid = 0;

		log_begin_time = new Timestamp(0);
		log_end_time   = new Timestamp(0);

		this.src_con  = src_con;
		this.dest_con = dest_con;
	}

	/*
	private boolean checkOrCreateSchema()
	{
		Statement stmt = null;
		boolean found = false;

		try {
			stmt = src_con.createStatement();
			ResultSet rs = stmt.executeQuery("SELECT nspname "
										   + "FROM pg_namespace "
										   + "WHERE nspname='postgresforest'");
			if ( rs.next() )
				found = true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}

		if ( found )
			return true;

		boolean result = false;
		try {
			stmt = src_con.createStatement();
			int rc = stmt.executeUpdate("CREATE SCHEMA postgresforest");
			if ( rc>0 )
				result = true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}

		return result;
	}
	*/

	/**
	 * 複製先DBへコミットを行う
	 *
	 * @return boolean 成功:true, 失敗:false
	 */
	public boolean commit_dest()
	{
		try {
			dest_con.commit();
		} catch (SQLException e) {
			return false;
		}
		return true;
	}

	/**
	 * 複製元DBへコミットを行う
	 *
	 * @return boolean 成功:true, 失敗:false
	 */
	public boolean commit_src()
	{
		try {
			src_con.commit();
		} catch (SQLException e) {
			return false;
		}
		return true;
	}

	/**
	 * 複製先DBへロールバックを行う
	 *
	 * @return boolean 成功:true, 失敗:false
	 */
	public boolean rollback_dest()
	{
		try {
			dest_con.rollback();
		} catch (SQLException e) {
			return false;
		}
		return true;
	}

	/**
	 * 複製元DBへロールバックを行う
	 *
	 * @return boolean 成功:true, 失敗:false
	 */
	public boolean rollback_src()
	{
		try {
			src_con.rollback();
		} catch (SQLException e) {
			return false;
		}
		return true;
	}

	/**
	 * ログテーブル中に存在しているログレコードの
	 * 最小トランザクションIDと最大トランザクションIDを取得し、
	 * その範囲にあるログレコードを開いてカーソルを定義する
	 *
	 * @return boolean 成功:true, 失敗:false
	 */
	public boolean openLogRecords()
		throws RecoveryException
	{
		ResultSet rs = null;

		getMaxMinXid();
		getMaxMinTimestamp(getLogMinXid(), getLogMaxXid());

		return openLogRecords(minLogXid, maxLogXid);
		//		return openLogRecords(log_begin_time, log_end_time);
	}

	private void getMaxMinXid() throws RecoveryException
	{
		getMaxMinXid(RecoveryManager.LOG_MINXID, RecoveryManager.LOG_MAXXID);
	}

	private void getMaxMinXid(int min, int max) throws RecoveryException
	{
		try {
			if ( src_con.getAutoCommit()==true )
				throw new RecoveryException("getMaxMinXid() can't be called "
											+ "under autocommit=true");
		} catch (Exception e) {
			throw new RecoveryException(e);
		}

		/* --- Get max/min xid of the log records in this snapshot. --- */
		try {
			Statement stmt = src_con.createStatement();

			ResultSet rs = stmt.executeQuery("SELECT max(xid), min(xid) "
										   + "  FROM postgresforest.log "
										   + " WHERE " + min + "<=xid "
										   + "   AND xid<=" + max);
			if ( rs.next() )
			{
				maxLogXid = rs.getInt(1);
				minLogXid = rs.getInt(2);
			}
			rs.close();

			stmt.close();
		} catch (Exception e) {
			e.printStackTrace();
			throw new RecoveryException("Can't get max/min values of "
									  + "the TransactionIDs.");
		}
	}

	private void getMaxMinTimestamp() throws RecoveryException
	{
		getMaxMinTimestamp(new Timestamp(0),
						   new Timestamp(System.currentTimeMillis()));
	}

	private void getMaxMinTimestamp(Timestamp minTime, Timestamp maxTime)
		throws RecoveryException
	{
		try {
			if ( src_con.getAutoCommit()==true )
				throw new RecoveryException("getMaxMinXid() can't be called "
											+ "under autocommit=true");
		} catch (Exception e) {
			throw new RecoveryException(e);
		}

		Logger.debug("getMaxMinTimestamp(): Timestamp [" + minTime
				   + "..." + maxTime + "] (input)");

		try {
			Statement stmt = src_con.createStatement();

			ResultSet rs = stmt.executeQuery("SELECT max(ltime), min(ltime) "
										   + "  FROM postgresforest.log "
										   + " WHERE '" + minTime + "'<=ltime "
										   + "   AND ltime<='" + maxTime + "'");
			if ( rs.next() )
			{
				log_end_time   = rs.getTimestamp(1);
				log_begin_time = rs.getTimestamp(2);
			}
			else
			{
				log_begin_time = new Timestamp(0);
				log_end_time = new Timestamp(System.currentTimeMillis());
			}
			rs.close();
			stmt.close();

			Logger.debug("getMaxMinTimestamp(): Timestamp ["
						 + log_begin_time + "..." + log_end_time
						 + "] (result)");
		} catch (Exception e) {
			throw new RecoveryException("Can't get max/min timestamp values");
		}
	}

	private void getMaxMinTimestamp(int minXid, int maxXid)
		throws RecoveryException
	{
		try {
			if ( src_con.getAutoCommit()==true )
				throw new RecoveryException("getMaxMinXid() can't be called "
											+ "under autocommit=true");
		} catch (Exception e) {
			throw new RecoveryException(e);
		}

		Logger.debug("getMaxMinTimestamp(): Xid [" + minXid
					 + "..." + maxXid + "] (input)");

		try {
			Statement stmt = src_con.createStatement();

			ResultSet rs = stmt.executeQuery("SELECT max(ltime), min(ltime) "
										   + "  FROM postgresforest.log "
										   + " WHERE " + minXid + "<=xid "
										   + "   AND xid<=" + maxXid + "");
			if ( rs.next() )
			{
				log_end_time   = rs.getTimestamp(1);
				log_begin_time = rs.getTimestamp(2);
			}

			if ( log_end_time==null || log_begin_time==null )
			{
				log_begin_time = new Timestamp(0);
				log_end_time = new Timestamp(System.currentTimeMillis());
			}
			rs.close();
			stmt.close();

			Logger.debug("getMaxMinTimestamp(): Timestamp ["
						 + log_begin_time + "..." + log_begin_time
						 + "] (result)");
		} catch (Exception e) {
			throw new RecoveryException("Can't get max/min timestamp values");
		}
	}

	/**
	 * 指定された範囲のすべてのテーブルのトランザクションIDの
	 * ログレコードを開いてカーソルを定義する。
	 *
	 * @param minXid 下限トランザクションID
	 * @param maxXid 上限トランザクションID
	 *
	 * @return boolean 成功:true, 失敗:false
	 */
	public boolean openLogRecords(int minXid, int maxXid)
		throws RecoveryException
	{
		return openLogRecords(minXid, maxXid, null);
	}

	/**
	 * 指定された範囲の指定されたテーブルのトランザクションIDの
	 * ログレコードを開いてカーソルを定義する。
	 *
	 * @param minXid 下限トランザクションID
	 * @param maxXid 上限トランザクションID
	 * @param tableName テーブル名
	 *
	 * @return boolean 成功:true, 失敗:false
	 */
	public boolean openLogRecords(int minXid, int maxXid, String tableName)
		throws RecoveryException
	{
		getMaxMinXid(minXid, maxXid);
		getMaxMinTimestamp(getLogMinXid(), getLogMaxXid());

		Logger.debug("openLogRecords(): xid=[" + getLogMinXid()
					 + "..." + getLogMaxXid() + "]");

		//		assertTrue(readStmt!=null);
		if ( readStmt!=null )
			throw new RecoveryException("Log records already opened.");

		try {
			String q = "SELECT tableid,xid,cid,oper,arg,ltime"
				     + "  FROM postgresforest.log"
				     + " WHERE xid>=" + getLogMinXid()
				     + "   AND xid<=" + getLogMaxXid();

			/* If the table name is specified. */
			if ( tableName!=null )
				q = q + "   AND tableid=" + lookupTableOid(tableName);

			q = q + " ORDER BY ltime";
		
			readStmt
				= src_con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
										  ResultSet.CONCUR_READ_ONLY);

			logRecords
				= readStmt.executeQuery(q);

			logRecords.beforeFirst();

			numberOfLogRecords = -1;
		} catch (Exception e) {
			e.printStackTrace();
			throw new RecoveryException("Can't open log record(s).");
		}
		return true;
	}

	/**
	 * openLogRecords()で定義されたカーソルを使って、
	 * 複製元DBからログレコードを順方向に1件読む。
	 * ログレコードはDMLの書式で返却される。
	 *
	 * @return String ログレコードを適用するためのDML文(INSERT/UPDATE/DELETE)
	 */
	public String readLogRecord() throws RecoveryException
	{
		String query = null;

		try {
			if ( src_con.getAutoCommit()==true )
				throw new RecoveryException("readLogRecord() can't be called "
											+ "under autocommit=true");
		} catch (Exception e) {
			throw new RecoveryException(e);
		}

		//		assertTrue( logRecords==null );

		try {
			//			assertTrue( src_con.getAutoCommit()==true );

			if ( !logRecords.isAfterLast() && logRecords.next() )
			{
				if ( log_apply_begin_time==null )
					log_apply_begin_time = logRecords.getTimestamp(6);
				if ( log_apply_end_time==null )
					log_apply_end_time = logRecords.getTimestamp(6);
				if ( log_apply_end_time.before( logRecords.getTimestamp(6) ) )
					log_apply_end_time = logRecords.getTimestamp(6);

				//System.out.println("TABLE OID:" + logRecords.getInt(1));
				/* ----------------------------------
				 * Build a new DML with a log record.
				 * ----------------------------------
				 */
				if ( logRecords.getString(4).equals("I") )
				{
					query = "INSERT INTO "
						+ lookupTableName(logRecords.getInt(1))
						+ logRecords.getString(5);
				}
				else if ( logRecords.getString(4).equals("U") )
				{
					query = "UPDATE " + lookupTableName(logRecords.getInt(1))
						+ " " + logRecords.getString(5);
				}
				else if ( logRecords.getString(4).equals("D") )
				{
					query = "DELETE FROM "
						+ lookupTableName(logRecords.getInt(1))
						+ " WHERE " + logRecords.getString(5);
				}
				else
				{
					throw new RecoveryException("Unknown operation in"
												+ " a log record.");
				}

				Timestamp ltime = logRecords.getTimestamp(6);
				int xid = logRecords.getInt(2);

				query = "/* " + xid + "/" +ltime + " */ " + query;
			}
			else /* end of records */
				return null;
		} catch (Exception e) {
			throw new RecoveryException(e);
		}

		return query;
	}

	/**
	 * ログレコード操作用に開いているカーソルとステートメントを閉じ、
	 * トランザクションをコミットする。
	 *
	 * @return boolean 成功:true, 失敗:false
	 */
	public boolean closeLogRecords() throws RecoveryException
	{
		if ( logRecords==null )
			throw new RecoveryException("Records not opened. (logRecords)");
		if ( readStmt==null )
			throw new RecoveryException("Records not opened. (readStmt)");

		try {
			logRecords.close();
			logRecords = null;

			readStmt.close();
			readStmt = null;

			src_con.commit();

			Logger.debug("closeLogRecords(): xid=[" + getLogMinXid()
						 + "..." + getLogMaxXid() + "]");
		} catch (SQLException e) {
			throw new RecoveryException("SQL error occured.");
		}

		log_apply_begin_time = null;
		log_apply_end_time   = null;

		return true;
	}

	/**
	 * ログレコードの適用開始にあたって、複製先DBをautocommit=offとする。
	 *
	 * @return boolean true(失敗の場合は例外が発生)
	 */
	public boolean beginApplyingLogRecords() throws RecoveryException
	{
		try {
			dest_con.setAutoCommit(false);
		} catch (Exception e) {
			throw new RecoveryException(e);
		}
		return true;
	}

	/**
	 * ログレコード（DML文）1件を、複製先のDBに適用する
	 *
	 * @param log ログレコード（INSERT/DELETE/UPDATE文）
	 *
	 * @return boolean 成功:true, 失敗:false	 
	 */
	public boolean applyLogRecord(String log) throws RecoveryException
	{
		int rc = 0;
		try {
			Statement stmt = dest_con.createStatement();
			rc = stmt.executeUpdate(log);
		} catch (Exception e) {
			throw new RecoveryException("applyLogRecord() failed. - " + log);
		}

		if ( rc>0 )
			return true;

		return false;
	}

	/**
	 * ログレコードの適用完了にあたって、複製先DBにCOMMITする。
	 *
	 * @return boolean true(失敗の場合は例外が発生)
	 */
	public boolean commitApplyingLogRecords() throws RecoveryException
	{
		try {
			dest_con.commit();
		} catch (Exception e) {
			throw new RecoveryException(e);
		}
		return true;
	}

	/**
	 * OIDからテーブル名を調べる。Hashtableにあればそこから持ってくる。
	 * Hashtableに無ければシステムカタログを検索し、
	 * 結果をHashtableに格納する。
	 *
	 * @param tabid テーブルのOID
	 *
	 * @return String テーブル名
	 */
	private String lookupTableName(int tabid)
	{
		String tableName = null;

		if ( tableNames==null )
			tableNames = new Hashtable();
		if ( tableOids==null )
			tableOids = new Hashtable();

		tableName = (String)tableNames.get(new Integer(tabid));

		if ( tableName==null )
		{
			try {
				tableName = getTableName(tabid);
				tableNames.put(new Integer(tabid), tableName);

				// make reverse map (tablename -> oid)
				tableOids.put(tableName, new Integer(tabid));
			} catch (Exception e) {
				tableName = null;
			}
		}
		
		return tableName;
	}

	private int lookupTableOid(String tableName) throws RecoveryException
	{
		Integer tableOid = null;

		if ( tableNames==null )
			tableNames = new Hashtable();
		if ( tableOids==null )
			tableOids = new Hashtable();

		tableOid = (Integer)tableOids.get(tableName);

		if ( tableOid==null )
		{
			int oid = getTableOid(tableName);
			if ( oid>0 )
			{
				tableOid = new Integer(oid);

				tableNames.put(tableOid, tableName);
				tableOids.put(tableName, tableOid);
			}
			else
				throw new RecoveryException("lookupTableOid() failed.");
		}

		return tableOid.intValue();
	}

	/**
	 * 更新ログ保存用テーブルが存在しているかどうかをチェックする
	 *
	 * @return boolean 存在している:true, 存在していない:false
	 */
	public boolean existsLogTable()
	{
		Statement stmt = null;
		ResultSet rs = null;
		int rc;

		try {
			stmt = src_con.createStatement();

			rs = stmt.executeQuery("SELECT COUNT(c.relname)" +
								   " FROM pg_class c, pg_namespace n" +
								   " WHERE c.relname='log'" +
								   " AND c.relnamespace=n.oid" +
								   " AND n.nspname='postgresforest'");

			if ( rs.next() )
				rc = rs.getInt(1);
			else
				rc = 0;

			stmt.close();
		}
		catch (SQLException e)
		{
			e.printStackTrace();
			rc = 0;
		}

		if ( rc==1 )
			return true;

		return false;
	}

	/**
	 * ログレコード中の最小のトランザクションIDを取得する
	 *
	 * @return int トランザクションID
	 */
	public int getLogMinXid()
	{
		return minLogXid;
	}

	/**
	 * ログレコード中の最大のトランザクションIDを取得する
	 *
	 * @return int トランザクションID
	 */
	public int getLogMaxXid()
	{
		return maxLogXid;
	}

	/* ----------------------------------------
	 * Get a table name and put into Hashtable.
	 * ----------------------------------------
	 */
	private String getTableName(int tabid)
		throws Exception 
	{
		Statement stmt = src_con.createStatement();
		ResultSet rs = null;
		String tableName = null;

		try {
			rs = stmt.executeQuery("SELECT relname FROM pg_class WHERE oid="
								   + tabid);

			if ( rs.next() )
				tableName = rs.getString(1);
			else
				throw new Exception("No such table.");
		}
		catch (SQLException e)
		{
			throw new Exception(e);
		}

		return tableName;
	}

	/* ----------------------------------------
	 * Get a table oid and put into Hashtable.
	 * ----------------------------------------
	 */
	private int getTableOid(String tableName) throws RecoveryException 
	{
		Statement stmt = null;
		ResultSet rs = null;
		int oid = 0;

		try {
			stmt = src_con.createStatement();

			rs = stmt.executeQuery("SELECT oid FROM pg_class WHERE relname='"
								   + tableName + "'");

			if ( rs.next() )
				oid = rs.getInt(1);
			else
				throw new RecoveryException("No such table. - " + tableName);
		}
		catch (SQLException e)
		{
			throw new RecoveryException(e);
		}

		return oid;
	}

	public int removeLogRecords()
		throws RecoveryException
	{
		int num;
		int rc;

		try {
			if ( src_con.getAutoCommit()==true )
				throw new RecoveryException("readLogRecord() can't be called "
											+ "under autocommit=true");
		} catch (Exception e) {
			throw new RecoveryException(e);
		}

		if ( log_apply_begin_time==null ||
			 log_apply_end_time == null )
		{
			Logger.debug("Can't remove log records since no record has been read.");
			return 0;
		}

		try {
			String q = "DELETE FROM postgresforest.log"
				     + " WHERE ltime>='" + log_apply_begin_time + "'"
				     + "   AND ltime<='" + log_apply_end_time + "'";

			rc = readStmt.executeUpdate(q);

			Logger.debug("removeLogRecords(): "
						 + rc + " log records[ts="
						 + log_apply_begin_time + "..."
						 + log_apply_end_time + "] removed.");

			num = rc;
		}
		catch (SQLException e)
		{
			throw new RecoveryException(e);
		}

		return num;
	}

	/**
	 * 指定したトランザクションIDの範囲の更新ログを削除する。
	 *
	 * @param minxid 削除対象最小トランザクションID
	 * @param maxxid 削除対象最大トランザクションID
	 *
	 * @return int 削除したログレコード数
	 */
	public int removeLogRecords(int minxid, int maxxid)
		throws RecoveryException
	{
		return removeLogRecords(minxid, maxxid, null);
	}

	/**
	 * 指定したテーブルの指定したトランザクションIDの範囲の更新ログを削除する。
	 *
	 * @param minxid 削除対象最小トランザクションID
	 * @param maxxid 削除対象最大トランザクションID
	 * @param tableName 削除対象テーブル名
	 *
	 * @return int 削除したログレコード数
	 */
	public int removeLogRecords(int minxid, int maxxid, String tableName)
		throws RecoveryException
	{
		int rc = 0;
		int num = 0;

		try {
			if ( src_con.getAutoCommit()==true )
				throw new RecoveryException("readLogRecord() can't be called "
											+ "under autocommit=true");
		} catch (Exception e) {
			throw new RecoveryException(e);
		}

		try {
			String q = "DELETE FROM postgresforest.log"
				     + " WHERE xid>=" + minxid
				     + "   AND xid<=" + maxxid
				     + "   AND ltime>='" + log_begin_time + "'"
				     + "   AND ltime<='" + log_end_time + "'";

			if ( tableName!=null )
				q = q + "   AND tableid=" + lookupTableOid(tableName);

			rc = readStmt.executeUpdate(q);

			Logger.debug("removeLogRecords(): "
						 + rc + " log records[xid="
						 + minxid + "..." + maxxid + "] removed.");

			num = rc;
		}
		catch (SQLException e)
		{
			throw new RecoveryException(e);
		}

		return num;
	}


	/**
	 * ログテーブルに対してVACUUM処理を行う
	 * （removeLogRecords()などを呼び出した後に行う）
	 */
	public void vacuumLogRecords() throws RecoveryException
	{
		/*
		boolean prev = false;
		try {
			Statement stmt = src_con.createStatement();

			int rc = stmt.executeUpdate("VACUUM postgresforest.log");
			stmt.close();
		}
		catch (SQLException e)
		{
			throw new RecoveryException(e);
		}
		*/
	}

	/**
	 * 現在の更新ログレコード数を取得する
	 *
	 * @return int 更新ログレコード数
	 */
	public int getNumberOfLogRecords()
	{
		int numLog;

		if ( logRecords==null )
		{
			Logger.error("Log records not opened.");
			return -1;
		}

		if ( numberOfLogRecords>=0 )
			return numberOfLogRecords;

		numLog = 0;
		try {
			Statement stmt = src_con.createStatement();

			ResultSet rs = stmt.executeQuery("SELECT count(*)"
										   + "  FROM postgresforest.log"
										   + " WHERE xid>=" + minLogXid
										   + "   AND xid<=" + maxLogXid);
			if ( rs.next() )
				numLog = rs.getInt(1);
		}
		catch (SQLException e)
		{
			e.printStackTrace();
			numberOfLogRecords = -1;
			return numberOfLogRecords;
		}
		numberOfLogRecords = numLog;

		return numberOfLogRecords;
	}

	/**
	 * 更新ログのレコードが存在しているかどうかを得る
	 *
	 * @return boolean 存在している:true, 存在していない:false
	 */
	public boolean hasLogRecords() throws Exception
	{
		if ( getNumberOfLogRecords()>0 )
			return true;

		return false;
	}

	/**
	 * 複製元、複製先へのコネクションを両方閉じる。
	 */
	public void close()
	{
		try {
			src_con.close();
			dest_con.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 現在のトランザクションIDを取得する
	 *
	 * @return int トランザクションID
	 */
	public int getCurrentTransactionId()
	{
		Statement stmt = null;
		ResultSet rs = null;
		int xid = 0;

		try {
			stmt = src_con.createStatement();

			rs = stmt.executeQuery("SELECT postgresforest.current_transaction_id()");
			if ( rs.next() )
				xid = rs.getInt(1);

			stmt.close();
		}
		catch (Exception e)
		{
			e.printStackTrace();
			xid = -1;
		}

		return xid;
	}

	/**
	 * 更新ログ用テーブルを作成する
	 * （バックエンドのprepare_recovery_logging()を呼び出す）
	 *
	 * @param tableName トリガを設定するテーブル名
	 * @return 成功:true, 失敗:false
	 */
	public boolean prepareRecoveryLogging(String tableName)
		throws RecoveryException
	{
		Statement stmt = null;
		int rc = 0;

		if ( tableName==null || tableName.length()==0 )
			return false;

		if ( checkLogTable() )
		{
			Logger.notice("Log table already exists.");
			return true;
		}

		try {
			stmt = src_con.createStatement();

			rc = stmt.executeUpdate("CREATE TABLE postgresforest.log (" +
									"  ltime timestamp, tableid oid, \"xid\" int4," +
									"  \"cid\" cid, oper char, arg text) with oids");

			rc = stmt.executeUpdate("CREATE INDEX log_ltime_idx on postgresforest.log(ltime)");
		
			stmt.close();
		}
		catch (Exception e)
		{
			e.printStackTrace();
			return false;
		}

		if ( !checkLogTable() )
			throw new RecoveryException("Log table not found.");

		return true;
	}


	private boolean checkLogTrigger(String tableName)
		throws RecoveryException
	{
		boolean rc = false;

		try {
			Statement stmt = src_con.createStatement();
			ResultSet rs = null;

			rs = stmt.executeQuery("SELECT COUNT(*) FROM pg_trigger t,pg_class c" +
								   " WHERE t.tgrelid=c.oid " +
								   "   AND t.tgname='" + tableName + "_logger' " +
								   "   AND c.relname='" + tableName + "'");

			rs.next();
			if ( rs.getInt(1)==1 )
				rc = true;

			rs.close();
		}
		catch (Exception e)
		{
			throw new RecoveryException(e);
		}

		return rc;
	}

	private boolean checkLogTable() throws RecoveryException
	{
		Statement stmt = null;
		boolean rc = false;

		try {
			stmt = src_con.createStatement();

			ResultSet rs = stmt.executeQuery("SELECT count(*) "
											 + " FROM pg_class c, pg_namespace n "
											 + "WHERE c.relnamespace=n.oid "
											 + "  AND nspname='postgresforest' "
											 + "  AND c.relname='log'");

			if ( rs.next() && rs.getInt(1)==1 )
				rc = true;

			rs.close();
			stmt.close();
		} catch (Exception e) {
			throw new RecoveryException(e);
		}

		return rc;
	}

	/**
	 * 複製対象テーブルに、更新ログをログテーブルに保存するためのトリガを
	 * 設定する（バックエンドのbegin_recovery_logging()を呼び出す）。
	 *
	 * @param tableName トリガを設定するテーブル名
	 *
	 * @return 成功:トランザクションID, 失敗:-1
	 */
	public int beginRecoveryLogging(String tableName)
		throws RecoveryException
	{
		Statement stmt = null;
		int xid = 0;
		ResultSet rs;

		if ( tableName==null )
			return -1;

		Logger.debug("beginRecoveryLogging: " + tableName);
		
		CancelThread cancel = null;
		
		if ( be_pid>0 )
		{
			cancel = new CancelThread(be_pid,
									  timeout,
									  src_url,
									  src_user,
									  src_pass);
			cancel.start();
		}

		try {
			stmt = src_con.createStatement();
			rs = stmt.executeQuery("SELECT "
					+ "postgresforest.begin_recovery_logging('"
					+ tableName + "')");

			if ( rs.next() )
				xid = rs.getInt(1);

			stmt.close();
			src_con.commit();
		}
		catch (Exception e)
		{
			e.printStackTrace();
			return -1;
		}
		finally
		{
			if ( cancel!=null )
				cancel.disable();
		}

		return xid;
	}

	/**
	 * 複製対象テーブルから、更新ログをログテーブルに保存するためのトリガを
	 * 削除する（バックエンドのstop_recovery_logging()を呼び出す）。
	 *
	 * @param tableName トリガを削除するテーブル名
	 *
	 * @return 成功:true, 失敗:false
	 */
	public boolean stopRecoveryLogging(String tableName)
	{
		Statement stmt;
		int rc;

		if ( tableName==null )
			return false;

		CancelThread cancel = null;
		
		Logger.debug("be_pid: " + be_pid);
		Logger.debug("timeout: " + timeout);
		Logger.debug("src_url: " + src_url);
		Logger.debug("src_user: " + src_user);
		Logger.debug("src_pass: " + src_pass);

		if ( be_pid>0 )
		{
			cancel = new CancelThread(be_pid,
									  timeout,
									  src_url,
									  src_user,
									  src_pass);
			cancel.start();
		}

		try {
			if ( !checkLogTrigger(tableName) )
				return true;

			stmt = src_con.createStatement();

			rc = stmt.executeUpdate("DROP TRIGGER " + tableName + "_logger ON " + tableName);

			stmt.close();
			src_con.commit();
		}
		catch (Exception e)
		{
			Logger.error(e.getMessage());
			return false;
		}
		finally
		{
			if ( cancel!=null )
				cancel.disable();
		}

		if ( rc==1 )
			return true;

		return false;
	}

	/**
	 * リカバリ用ログテーブルの削除
	 * （バックエンドのcleanup_recovery_logging()を呼び出す）
	 *
	 * @return boolean 成功:true, 失敗:false
	 */
	public boolean cleanupRecoveryLogging()
		throws RecoveryException
	{
		String q = null;

		if ( !checkLogTable() )
		{
			Logger.notice("Log table does not exist.");
			return true;
		}

		try {
			Statement stmt = src_con.createStatement();

			int rc = stmt.executeUpdate("DROP TABLE postgresforest.log");

			stmt.close();

			src_con.commit();
		}
		catch (Exception e)
		{
			Logger.error(e.getMessage());
			throw new RecoveryException(e);
		}

		return true;
	}

	/**
	 * COPYコマンドでテーブルデータのダンプを外部ファイルに出力し、
	 * ダンプを取得した時のトランザクションIDを取得する。
	 *
	 * @param tableName テーブル名
	 * @param fileName 出力先ファイル名
	 *
	 * @return int 成功:トランザクションID, 失敗:-1
	 */
	public int copyTableToFile(String tableName, String fileName)
		throws RecoveryException
	{
		int xid = -1;

		boolean prev_mode = false;

        CancelThread cancel = null;

        if ( be_pid>0 )
        {
            cancel = new CancelThread(be_pid,
                                      timeout,
                                      src_url,
                                      src_user,
                                      src_pass);
			cancel.start();
        }

		try {
			Logger.debug("copyTableToFile(): " + tableName + " to " + fileName);

			Statement stmt = src_con.createStatement();

			int rc;

			rc = stmt.executeUpdate("COPY " + tableName + " TO '"
									+ fileName + "'");

			/*
			if ( rc>0 )
			{
				ResultSet rs = stmt.executeQuery("SELECT "
								 + "postgresforest.current_transaction_id()");

				if ( rs.next() )
					xid = rs.getInt(1);

				stmt.close();

				int tableOid = lookupTableOid(tableName);

				stmt = src_con.createStatement();
				stmt.executeUpdate("DELETE FROM postgresforest.log "
								 + " WHERE xid<" + xid
							     + "   AND tableid=" + tableOid);

				src_con.commit();
			}
			else
			{
				src_con.rollback();
				return -1;
			}
			*/

			Logger.debug("copyTableToFile(): done.");
		} catch (Exception e) {
			e.printStackTrace();
			throw new RecoveryException(e);
		}
		finally
		{
			if ( cancel!=null )
				cancel.disable();
		}

		return xid;
	}

	/**
	 * COPYコマンド出力ファイルから、テーブルにデータを入力する（未実装）。
	 *
	 * @return boolean 成功:true, 失敗:false
	 */
	public boolean copyTableFromFile(String tableName, String fileName)
	{
		return false;
	}

	public Connection getSourceConnection()
	{
		return src_con;
	}

	/*
	 * リモートのPostgreSQLサーバにあるファイルをローカルにコピーする
	 *
	 * @param String remoteFile リモートのファイルパス
	 * @param String localFile ローカルのファイルパス
	 *
	 * @return boolean true:成功, false:失敗
	 */
	public boolean copyFileToLocal(String remoteFile, String localFile)
	{
		Statement stmt = null;
		ResultSet rs = null;

		try {
			long size = 0;
			stmt = src_con.createStatement();

			/*
			 * ファイルサイズ取得
			 */
			rs = stmt.executeQuery("SELECT postgresforest.file_size('"
								   + remoteFile + "')");
			if ( rs.next() )
				size = rs.getLong(1);

			/*
			 * 出力ファイルを開く
			 */
			FileOutputStream fos = new FileOutputStream(new File(localFile));

			/*
			 * ファイルをブロックに分割して取得
			 */
			long left  = size;
			long off   = 0;
			long blksz = 1024*1024;

			long len = 0;
			for (left=size ; left>0 ; left-=len )
			{
				byte[] d = null;

				if ( left<blksz )
					len = left;
				else
					len = blksz;

				rs = stmt.executeQuery("SELECT postgresforest.file_read("
									   + "'" + remoteFile + "', "
									   + off + ", "
									   + len + ")");
				if ( rs.next() )
					d = rs.getBytes(1);

				fos.write(d);

				off += len;
			}

			/*
			 * 出力ファイルを閉じる
			 */
			fos.close();

			stmt.close();
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}

		return true;
	}

	/**
	 * 指定したテーブルに存在している外部制約を有効にする
	 *
	 * @param String[] tableNames テーブル名（複数指定可）
	 *
	 * @return boolean ひとつ以上を変更したらtrue、変更されなければfalse
	 */
    public boolean enableFKs(String[] tableNames)
		throws RecoveryException
    {
        return changeFKs(tableNames, "ENABLE");
    }

    /**
     * 指定したテーブルに存在している外部制約を無効にする
     *
     * @param String[] tableNames テーブル名（複数指定可）
     *
     * @return boolean ひとつ以上を変更したらtrue、変更されなければfalse
     */
	public boolean disableFKs(String[] tableNames)
		throws RecoveryException
	{
		return changeFKs(tableNames, "DISABLE");
	}

	private boolean changeFKs(String[] tableNames, String op)
		throws RecoveryException
	{
		int rc = 0;

		try {
			for (int i=0 ; i<tableNames.length ; i++)
			{
				Statement stmt = null;

				stmt = dest_con.createStatement();

				rc = stmt.executeUpdate("ALTER TABLE " + tableNames[i]
										+ " " + op + " TRIGGER ALL");

				stmt.close();
			}

			dest_con.commit();
		}
		catch (Exception e)
		{
			throw new RecoveryException(e);
		}

		if ( rc>0 )
			return true;

		return false;
	}

	/**
	 * 指定したテーブルをTRUNCATEする。
	 * 複数テーブルを一括してTRUNCATEできる（外部制約などがある場合）。
	 *
	 * @param String[] tableNames TRUNCATE対象テーブル名（複数指定可）
	 *
	 * @return boolean 成功すればtrue
	 */
	public boolean truncateTables(String[] tableNames)
		throws RecoveryException
	{
		int rc = 0;

        try {
			Statement stmt = dest_con.createStatement();

			String tableName = "";

			for (int i=0 ; i<tableNames.length ; i++)
			{
				if ( i==0 )
					tableName = tableNames[i];
				else
					tableName = tableName + "," + tableNames[i];
			}

			rc = stmt.executeUpdate("TRUNCATE TABLE " + tableName); 
        } catch (SQLException e) {
            throw new RecoveryException(e);
        }

		return true;
	}

	/**
	 * リカバリ先のテーブルをダンプファイルからレストアする
	 *
	 * @param String tableName テーブル名
	 * @param String fileName ダンプファイル名
	 *
	 * @return boolean 成功:true, 失敗:false
	 */
	public boolean rebuildTableFromFile(String tableName, String fileName)
		throws RecoveryException
	{
		int rc = 0;

		Logger.debug("rebuildTableFromFile(): " + tableName + " from " + fileName);

		ArrayList tglist = new ArrayList();

        CancelThread cancel = null;

        if ( be_pid>0 )
        {
            cancel = new CancelThread(be_pid,
                                      timeout,
                                      src_url,
                                      src_user,
                                      src_pass);
			cancel.start();
        }

		try {

			Statement stmt = dest_con.createStatement();

			rc = stmt.executeUpdate("LOCK " + tableName
									+ " IN ACCESS EXCLUSIVE MODE");
			if ( rc!=0 )
				throw new RecoveryException("rebuildTableFromFile(): "
											+ "LOCK command failed.");

			rc = stmt.executeUpdate("COPY " + tableName + " FROM '"
									+ fileName + "'");
			if ( rc!=0 )
				throw new RecoveryException("rebuildTableFromFile(): "
											+ "COPY command failed.");

		} catch (SQLException e) {
			throw new RecoveryException(e);
		}
		finally
		{
			if ( cancel!=null )
			{
				cancel.disable();
			}
		}

		Logger.debug("rebuildTableFromFile(): done.");

		return true;
	}

	/**
	 * すべてのGSCへの接続を確立する
	 *
	 * @return boolean 成功すればtrue, 失敗するとRecoveryException
	 */
	public boolean connectToGSC(String url, String user, String pass)
		throws RecoveryException
	{
		Connection con = null;

		gsc_url  = url;
		gsc_user = user;
		gsc_pass = pass;

		try {
			con = DriverManager.getConnection(url, user, pass);

			Statement stmt = con.createStatement();
			
			ResultSet rs = stmt.executeQuery("SELECT s.url,g.dbname "
								       + "FROM forest_server s, forest_gsc g "
									   + "WHERE s.serverid=g.serverid");

			ArrayList list = new ArrayList();

			while ( rs.next() )
				list.add( "jdbc:postgresql:" + rs.getString(1) + rs.getString(2) );

			gsc_urls = new String[list.size()];

			for (int i=0 ; i<gsc_urls.length ; i++)
				gsc_urls[i] = (String)list.get(i);

			rs.close();

			/*
			 * Get a cache refresh interval
			 */
			rs = stmt.executeQuery("SELECT cache_reflesh "
								 + "  FROM forest_config "
								 + " WHERE configid='" + configId + "'");

			if ( rs!=null && rs.next() )
				cache_refresh = rs.getInt(1);

			rs.close();

			stmt.close();
			con.close();
			/*
			 * I know a connection to same database will be established later,
			 * but it's ok.
			 */
		} catch (SQLException e) {
			throw new RecoveryException(e);
		}

		Logger.debug("" + gsc_urls.length + " GSC(s) found.");

		return connectAllGSCs();
	}

	public int getCacheRefresh()
	{
		return cache_refresh;
	}

	public int getActiveTransactions()
	{
		int n = -1;

		try {
			Statement stmt = src_con.createStatement();
			ResultSet rs
				= stmt.executeQuery("SELECT postgresforest.get_active_transactions()");
			if ( rs.next() )
				n = rs.getInt(1);

		} catch (Exception e) {
			e.printStackTrace();
			return -1;
		}

		return n;
	}

	private boolean connectAllGSCs() throws RecoveryException
	{
		gsc_con = new Connection[gsc_urls.length];

		for (int i=0 ; i<gsc_urls.length ; i++)
		{
			gsc_con[i] = null;
			try {
				//				System.out.println(gsc_urls[i]);
				gsc_con[i] = DriverManager.getConnection(gsc_urls[i],
														 gsc_user,
														 gsc_pass);

			} catch (SQLException e) {
				disconnectAllGSCs();
				throw new RecoveryException(e);
			}
		}

		return true;
	}

	/**
	 * すべてのGSCへの接続を終了する
	 *
	 * @return boolean 成功すればtrue, 失敗するとRecoveryException
	 */
	public boolean disconnectFromGSC() throws RecoveryException
	{
		return disconnectAllGSCs();
	}

	private boolean disconnectAllGSCs() throws RecoveryException
	{
		for (int i=0 ; i<gsc_con.length ; i++)
		{
			try {
				if ( gsc_con[i]!=null )
					gsc_con[i].close();
			} catch (SQLException e) {
				throw new RecoveryException(e);
			}
		}
		return true;
	}

	private int executeUpdateGSC(String query)
		throws SQLException, RecoveryException
	{
		int rc = 0;
		int rc_prev = -1;

		if ( gsc_con.length==0 )
			throw new RecoveryException("No connection to the GSC is established.");

		for (int i=0 ; i<gsc_con.length ; i++)
		{
			if ( gsc_con[i]==null )
				continue;

			Statement stmt = gsc_con[i].createStatement();

			rc = stmt.executeUpdate(query);

			if ( rc_prev>=0 && rc!=rc_prev )
				throw new RecoveryException("Update result(s) would be inconsistent.");

			rc_prev = rc;


			int rc2
				= stmt.executeUpdate("UPDATE forest_config " +
									 "   SET update_date=now() " +
									 " WHERE configid='" + configId + "'");
		}


		return rc;
	}
	
	private int getAliveServerId() throws RecoveryException
	{
		int id = -1;

		for (int i=0 ; i<gsc_con.length ; i++)
		{
			try {
				Statement stmt = gsc_con[i].createStatement();

				ResultSet rs = stmt.executeQuery("SELECT serverid "
											   + "  FROM forest_server "
											   + " WHERE status=1 LIMIT 1");
				if ( rs.next() )
					id = rs.getInt(1);

				rs.close();
				stmt.close();
			} catch (Exception e) {
				/*
				 * Don't throw an exception here,
				 * because we attempt to check all GSCs.
				 *
				 * Exception should be thrown later.
				 */
				e.printStackTrace();
			}
			if ( id>=0 )
				break;
		}

		if ( id<0 )
			throw new RecoveryException("Can't find an alive server.");

		return id;
	}

	public int getServerStatus(int serverId) throws RecoveryException
	{
		int status = -10;
		try {
			Statement stmt = null;
			for (int i=0 ; stmt==null && i<gsc_con.length ; i++)
				stmt = gsc_con[i].createStatement();

			ResultSet rs = stmt.executeQuery("SELECT status "
										   + "  FROM forest_server "
										   + " WHERE serverid=" + serverId);

			if ( rs.next() )
				status = rs.getInt(1);
		} catch (Exception e) {
			throw new RecoveryException(e);
		}
		if ( status==-10 )
			throw new RecoveryException("Can't find serverId. - " + serverId);

		return status;
	}

	public void setServerStatus(int serverId, int status)
		throws RecoveryException
	{
		int rc = 0;
		try {
			rc = executeUpdateGSC("UPDATE forest_server SET status=" + status + " WHERE serverid=" + serverId);
		} catch (SQLException e) {
			throw new RecoveryException(e);
		}

		if ( rc==0 )
			throw new RecoveryException("No GSC updated.");
	}

	public boolean setTableStatus(int serverId, int status)
		throws RecoveryException
	{
		int rc = 0;
		String query = "UPDATE forest_tablepart SET status=" + status + " "
			+ "WHERE dbname IN ( SELECT dbname FROM "
			+ "  forest_servdb WHERE serverid=" + serverId + ")";

		try {
			rc = executeUpdateGSC(query);
		} catch (SQLException e) {
			throw new RecoveryException(e);
		}

		if ( rc>0 )
			return true;

		return false;
	}

	public boolean acquireAllTableLock(int serverId) throws RecoveryException
	{
		return setTableStatus(serverId, TableInfo.TABLE_UNAVAILABLE);
	}

	public boolean releaseAllTableLock(int serverId) throws RecoveryException
	{
		return setTableStatus(serverId, TableInfo.TABLE_AVAILABLE);
	}

	public int checkGscServdbEntry(String dbname, int serverid)
	{
		String q = "SELECT dbno FROM forest_servdb " +
			       " WHERE dbname='" + dbname + "'" +
			       "   AND serverid=" + serverid;

		int dbno = -1;

		for (int i=0 ; i<gsc_con.length ; i++)
		{
			try {
				Statement stmt = gsc_con[i].createStatement();

				ResultSet rs = stmt.executeQuery(q);

				if ( rs!=null && rs.next() )
				{
					dbno = rs.getInt(1);
					rs.close();
				}
				stmt.close();
				break;
			} catch (Exception e) {
				/*
				 * Don't throw an exception here,
				 * because we attempt to check all GSCs.
				 *
				 * Exception should be thrown later.
				 */
				e.printStackTrace();
			}
		}

		return dbno;
	}

	public int createGscServdbEntry(String dbname, int new_serverid)
	{
		String q = "SELECT max(dbno) FROM forest_servdb";

		int new_dbno = -1;

		for (int i=0 ; i<gsc_con.length ; i++)
		{
			try {
				Statement stmt = gsc_con[i].createStatement();

				ResultSet rs = stmt.executeQuery(q);

				if ( rs!=null && rs.next() )
				{
					new_dbno = rs.getInt(1);
					rs.close();
				}
				stmt.close();
				break;
			} catch (Exception e) {
				/*
				 * Don't throw an exception here,
				 * because we attempt to check all GSCs.
				 *
				 * Exception should be thrown later.
				 */
				e.printStackTrace();
			}
		}

		new_dbno++;
		
		q = "INSERT INTO forest_servdb VALUES (" + new_dbno + ",'" + dbname + "'," + new_serverid + ")";
		Logger.debug(q);

		try {
			executeUpdateGSC(q);
		}
		catch (Exception e)
		{
			Logger.error(e.getMessage());
			return -1;
		}

		return new_dbno;
	}

	public boolean checkGscTablepartdtlEntry(int dbno,
											 String dbname,
											 String table_name)
	{
		String q = "SELECT table_name FROM forest_tablepartdtl " +
			       " WHERE dbno=" + dbno + 
			       "   AND dbname='" + dbname + "'" +
			       "   AND table_name='" + table_name + "'";

		String table = null;

		for (int i=0 ; i<gsc_con.length ; i++)
		{
			try {
				Statement stmt = gsc_con[i].createStatement();

				ResultSet rs = stmt.executeQuery(q);

				if ( rs!=null && rs.next() )
				{
					table = rs.getString(1);
					rs.close();
				}
				stmt.close();
				break;
			} catch (Exception e) {
				/*
				 * Don't throw an exception here,
				 * because we attempt to check all GSCs.
				 *
				 * Exception should be thrown later.
				 */
				e.printStackTrace();
			}
		}

		if ( table!=null )
			return true;

		return false;
	}

	public boolean createGscTablepartdtlEntry(int src_dbno,
											  int dest_dbno,
											  String dbname,
											  String tableName)
	{
		String q = "SELECT dbno,dbname,table_name,part_no,priority" +
			       "  FROM forest_tablepartdtl " +
			       " WHERE dbno=" + src_dbno + 
			       "   AND dbname='" + dbname + "'" +
			       "   AND table_name='" + tableName + "'";

		String table = null;

		for (int i=0 ; i<gsc_con.length ; i++)
		{
			try {
				Statement stmt = gsc_con[i].createStatement();

				ResultSet rs = stmt.executeQuery(q);

				while ( rs!=null && rs.next() )
				{
					String qq = "INSERT INTO forest_tablepartdtl VALUES (" +
						             dest_dbno + "," +
						             "'" + dbname + "'," +
						             "'" + rs.getString(3) + "'," +
						             rs.getInt(4) + "," +
						             rs.getInt(5) + ")";

					Logger.debug(qq);

					executeUpdateGSC(qq);
				}
				rs.close();
				stmt.close();
				break;
			} catch (Exception e) {
				/*
				 * Don't throw an exception here,
				 * because we attempt to check all GSCs.
				 *
				 * Exception should be thrown later.
				 */
				e.printStackTrace();
			}
		}

		if ( table!=null )
			return true;

		return false;
	}

	/**
	 * 物理テーブル名の一覧をGSCから取得する。
	 * パーティション化されている場合には、各パーティションの名前を取得する。
	 *
	 * @param String dbName データベース名
	 * @return String[] テーブル名のString配列
	 */
	public String[] getPhysicalTableNames(String dbName)
		throws RecoveryException
	{
		ArrayList a = new ArrayList();

		/* -- replicated tables -- */
		String q = null;

		/*
		 * 多重化テーブル、パーティションテーブル１のみを取得
		 */
		q = "SELECT table_name,part_type,part_count " +
			"  FROM forest_tablepart " +
			" WHERE dbname='" + dbName + "'" +
			"   AND ( part_type=0 OR part_type=1 )";
		
		for (int i=0 ; i<gsc_con.length ; i++)
		{
			try {
				Statement stmt = gsc_con[i].createStatement();

				ResultSet rs = stmt.executeQuery(q);

				if ( rs!=null )
				{
					while ( rs.next() )
					{
						if ( rs.getInt(2)==0 )      /* replicated table */
						{
							a.add( rs.getString(1) );
						}
						else if ( rs.getInt(2)==1 )	/* partitioned table */
						{
							/* -- add all partitions with suffix -- */
							for (int j=0 ; j<rs.getInt(3) ; j++)
							{
								String suffix = "_";
								if ( j<10 )
									suffix = suffix + "0" + j;
								else
									suffix = suffix + j;

								a.add( rs.getString(1) + suffix );
							}
						}
					}
					
					rs.close();

					// いずれかのノードのGSCで成功すれば終了
					break;
				}

				stmt.close();
			} catch (Exception e) {
				/*
				 * Don't throw an exception here,
				 * because we attempt to check all GSCs.
				 *
				 * Exception should be thrown later.
				 */
				e.printStackTrace();
			}
		}

		if ( a.size()==0 )
			return null;

		String[] tableNames = new String[a.size()];

		for (int i=0 ; i<a.size() ; i++)
			tableNames[i] = (String)a.get(i);

		return tableNames;
	}

	/**
	 * 論理テーブル名の一覧をGSCから取得する。
	 * パーティション化されている場合には、UNION VIEWの名前を取得する。
	 *
	 * @param String dbName データベース名
	 * @return String[] テーブル名のString配列
	 */
	public String[] getLogicalTableNames(String dbName)
		throws RecoveryException
	{
		ArrayList a = new ArrayList();

		/* -- replicated tables -- */
		String q = null;

		q = "SELECT table_name,part_type,part_count " +
			"  FROM forest_tablepart " +
			" WHERE dbname='" + dbName + "'";
		
		for (int i=0 ; i<gsc_con.length ; i++)
		{
			try {
				Statement stmt = gsc_con[i].createStatement();

				ResultSet rs = stmt.executeQuery(q);

				if ( rs!=null )
				{
					while ( rs.next() )
					{
						a.add( rs.getString(1) );
					}
					
					rs.close();
				}

				stmt.close();

				// いずれかのノードのGSCで成功すれば終了
				break;
			} catch (Exception e) {
				/*
				 * Don't throw an exception here,
				 * because we attempt to check all GSCs.
				 *
				 * Exception should be thrown later.
				 */
				e.printStackTrace();
			}
		}

		if ( a.size()==0 )
			return null;

		String[] tableNames = new String[a.size()];

		for (int i=0 ; i<a.size() ; i++)
			tableNames[i] = (String)a.get(i);

		return tableNames;
	}

	public String getUrlFromServerId(int serverId)
		throws RecoveryException
	{
		int id = -1;
		String serverUrl = null;

		String q = "SELECT url FROM forest_server WHERE serverId=" + serverId;

		for (int i=0 ; i<gsc_con.length ; i++)
		{
			try {
				Statement stmt = gsc_con[i].createStatement();

				ResultSet rs = stmt.executeQuery(q);
				if ( rs.next() )
					serverUrl = rs.getString(1);

				rs.close();
				stmt.close();
			} catch (Exception e) {
				/*
				 * Don't throw an exception here,
				 * because we attempt to check all GSCs.
				 *
				 * Exception should be thrown later.
				 */
				e.printStackTrace();
			}
			if ( serverUrl != null )
				break;
		}

		if ( serverUrl == null )
			throw new RecoveryException("Can't find an alive server.");

		return "jdbc:postgresql:" + serverUrl;
	}

	/**
	 * DBのスナップショットを取り、指定テーブルすべての
	 * テーブルデータの外部ファイルへのダンプを1トランザクションで行う。
	 * スナップショットを取得した時のトランザクションIDを取得する。
	 *
	 * @param tableNames ダンプ対象テーブル名配列
	 * @param tableFilesfileNames 出力先ファイル名配列
	 *
	 * @return int 成功:トランザクションID, 失敗:-1
	 */
	public int createSnapshotCopy(String[] tableNames, String tableFiles[])
		throws RecoveryException
	{
		int xid = -1;
		
		try
		{
			src_con.setAutoCommit(false);
			src_con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
				
			//Wait any other transaction's end.
			//As trigger has already set, get return status "WARNING".
			xid = beginRecoveryLogging(tableNames[0]); 

			for (int i=0 ; i<tableNames.length ; i++)
			{
				Logger.notice("  COPYING " + tableNames[i] + "");
				copyTableToFile(tableNames[i], tableFiles[i]);
				Logger.notice("    done.");
			}
			
			src_con.commit();
		} catch (Exception e) {
			throw new RecoveryException(e);
		}
		
		return xid;
	}

	public int getTableStatus(int serverId)
		throws RecoveryException
	{
		int status = 0;
		
		String query = "SELECT max(status) FROM forest_tablepart "
			+ "WHERE dbname IN ( SELECT dbname FROM "
			+ "  forest_servdb WHERE serverid=" + serverId + ")";
		
		for (int i=0 ; i<gsc_con.length ; i++)
		{
			try {
				Statement stmt = gsc_con[i].createStatement();

				ResultSet rs = stmt.executeQuery(query);
				
				if ( rs!=null && rs.next() )
				{
					status = rs.getInt(1);
					rs.close();
				}
				stmt.close();
				break;
			} catch (Exception e) {
				/*
				 * Don't throw an exception here,
				 * because we attempt to check all GSCs.
				 *
				 * Exception should be thrown later.
				 */
				e.printStackTrace();
			}
		}
		
		return status;
	}

	public void closeAllConnections()
	{
		try {
			dest_con.close();
			src_con.close();
			for (int i=0 ; i<gsc_con.length ; i++)
			{
				gsc_con[i].close();
			}
		}
		catch (Exception e)
		{
			Logger.error("ERROR: " + e.getMessage());
		}
	}
}
