import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;

/**
* ＤＢにアクセスするためのクラス
*/
public class DBAccessor implements Cloneable {
  /** エラーをどこにも出力しない */
  final public static int TO_NO = 0;
  /** エラーをコンソールに出力する */
  final public static int TO_CONSOLE = 1;
  /** エラーをLogを使って出力する */
  final public static int TO_LOG = 2;
  /** エラーをDialogに出力する */
  final public static int TO_DIALOG = 3;
  
  /** データを読み込むモード */
  public static enum MODE {
    ALL, //全データを読み込む
    ONE  //１レコードだけ読み込む
  }
  
  /** エラー時にログの出力先 */
  protected int errorLog = TO_CONSOLE;
  
  /**  データベースコネクション */
  protected Connection con = null;
  /**  接続文字列(URL) */
  protected String connectionString = null;
  /**  ドライバ名 */
  protected String driverName = null;
  /**  パスワード */
  protected String pass = null;
  
  private static DBAccessor self = null;
  /**  ユーザ名 */
  protected String user = null;
  
  
  /**
  * コンストラクタ。
  *
  *@param  driverName        DBドライバ名
  *@param  connectionString  DB接続文字列（URL）
  *@param  user              ユーザー名
  *@param  pass              パスワード
  */
  public DBAccessor(String driverName, String connectionString, String user, String pass) {
    this(driverName, connectionString, user, pass, TO_CONSOLE);
  }
  
  /**
  * コンストラクタ。
  *
  *@param  driverName        DBドライバ名
  *@param  connectionString  DB接続文字列（URL）
  *@param  user              ユーザー名
  *@param  pass              パスワード
  *@param  errorLog          エラーの出力先。TO_NO、TO_CONSOLE、TO_LOG、TO_DIALOGのいずれか
  */
  public DBAccessor(String driverName, String connectionString, String user, String pass, int errorLog) {
    this.errorLog = errorLog;
    this.driverName = driverName;
    this.connectionString = connectionString;
    this.user = user;
    this.pass = pass;
    
    try {
      Class.forName(driverName);
    } catch (Exception e) {
      this.outputException(e);
    }
  }
  
  
  @Override
  public Object clone(){
    return new DBAccessor(
      this.driverName,
      this.connectionString,
      this.user,
      this.pass,
      this.errorLog);
  }

  
  
  /**  接続を閉じる */
  public void close() {
    if (!isClosed()) {
      try {
        con.close();
      } catch (Exception e) {
        this.outputException(e);
      }
      con = null;
    }
  }
  
  /**
  * データベースに接続し、SQL 文をデータベースに送るための Statement オブジェクトを生成します。
  *
  *@return    新しいデフォルト Statement オブジェクト
  *@exception  SQLException  データベースアクセスエラーが発生した場合、このメソッドがクローズされた接続に対して呼び出された場合、または指定されたパラメータが型および並行処理を示す ResultSet 定数でない場合 
  */
  public Statement createStatement() throws SQLException{
    if (connect()){
      return con.createStatement();
    }
    return null;
  }
  
  /**
  * データベースに接続し、パラメータ付き SQL 文をデータベースに送るための PreparedStatement オブジェクトを生成します。  
  *
  *@param  sql  1 つ以上の '?' IN パラメータプレースホルダーを含めることができる SQL 文 
  *@return    プリコンパイルされた SQL 文を含む新しいデフォルトの PreparedStatement オブジェクト 
  *@exception  SQLException  データベースアクセスエラーが発生した場合、またはこのメソッドがクローズされた接続について呼び出された場合 
  */
  public PreparedStatement prepareStatement(String sql) throws SQLException {
    if (connect()){
      return con.prepareStatement(sql);
    }
    return null;
  }
  
  /**
  * データベースのストアドプロシージャーを呼び出すための CallableStatement オブジェクトを生成します
  *
  *@param  sql - 1 つ以上の '?' パラメータプレースホルダーを含めることができる SQL 文。 通常この文は、JBDC 呼び出しのエスケープ構文を使用して指定される
  *@return    プリコンパイルされた SQL 文を含む新しいデフォルトの CallableStatement オブジェクト
  *@exception  SQLException  データベースアクセスエラーが発生した場合、またはこのメソッドがクローズされた接続について呼び出された場合 
  */
  public CallableStatement prepareCall(String sql) throws SQLException {
    if (connect()){
      return con.prepareCall(sql);
    }
    return null;
  }
  
  /**
  * 指定された Statmentクラス インスタンスを解放します。  
  *
  *@param  st  Statmentクラス インスタンス
  */
  public void close(Statement st){
    if (st != null){
      try {
        st.close();
      } catch (Exception e){
        this.outputException(e);
      }
    }
  }
  
  /**
  * 指定された ResultSetクラス インスタンスを解放します。  
  *
  *@param  rs  ResultSetクラス インスタンス
  */
  public void close(ResultSet rs){
    if (rs != null){
      try {
        rs.close();
      } catch (Exception e){
        this.outputException(e);
      }
    }
  }
  
  /**
  * 指定された Statmentクラス インスタンス と ResultSetクラス インスタンス を解放します。  
  *
  *@param  st  Statmentクラス インスタンス
  *@param  rs  ResultSetクラス インスタンス
  */
  public void close(Statement st, ResultSet rs){
    close(rs);
    close(st);
  }
  
  /**
  * コミット処理をする
  *
  *@return    コミット処理に成功した場合はTrue、それ以外はFalse。
  */
  public boolean commit() {
    if (isClosed()) {
      return false;
    }
    
    try {
      con.commit();
      return true;
    } catch (Exception e) {
      this.outputException(e);
      return false;
    }
  }
  
  
  
  /**
  *  データベース接続
  *
  *@return    接続に成功した場合はTrue、それ以外はFalse。
  */
  public boolean connect() throws SQLException{
    boolean result = false;
    if (isClosed()) {
      con = DriverManager.getConnection(connectionString,
        user, pass);
      con.setAutoCommit(false);
      result = true;
    } else {
      // Log.getLogger().severe("既に接続されています");
      result = true;
    }
    return result;
  }
  
  
  /**
  *  指定されたパラメータからDBAccessorインスタンスを返す。
  *  getInstance() を使用することにより、生成された
  *  インスタンスを取り出すことが出来る。<br><br>
  *  もし、以前に生成されたインスタンスがあれば接続を閉じる。
  *
  *@param  driverName        DBドライバ名
  *@param  connectionString  DB接続文字列（URL）
  *@param  user              ユーザー名
  *@param  pass              パスワード
  *@return                   生成したDBAccessorのインスタンス
  */
  public static DBAccessor createInstance(String driverName, String connectionString, String user, String pass) {
    return createInstance(driverName, connectionString, user, pass, TO_CONSOLE);
  }
  
  /**
  *  指定されたパラメータからDBAccessorインスタンスを返す。
  *  getInstance() を使用することにより、生成された
  *  インスタンスを取り出すことが出来る。<br><br>
  *  もし、以前に生成されたインスタンスがあれば接続を閉じる。
  *
  *@param  driverName        DBドライバ名
  *@param  connectionString  DB接続文字列（URL）
  *@param  user              ユーザー名
  *@param  pass              パスワード
  *@param  errorLog          エラーの出力先。TO_NO、TO_CONSOLE、TO_LOG、TO_DIALOGのいずれか
  *@return                   生成したDBAccessorのインスタンス
  */
  public static DBAccessor createInstance(String driverName, String connectionString, String user, String pass, int errorLog) {
    if (self != null && !self.isClosed()){
      self.close();
    }
    self = new DBAccessor(driverName, connectionString, user, pass, errorLog);
    return getInstance();
  }
  
  
  
  /**
  *  createInstance()メソッドによって生成されたインスタンスを
  *  取得する。
  *
  *@return    createInstanceによって生成されたDBAccessorインスタンス
  */
  public static DBAccessor getInstance() {
    return self;
  }
  
  
  /**
  *  createInstance()メソッドによって生成されたインスタンスから
  *  新しいDBAccessorを返す
  *
  *@return    createInstanceによって生成されたDBAccessorインスタンスから
  *           生成された新しいDBAccessor
  */
  public static DBAccessor newInstance() {
    return (DBAccessor)getInstance().clone();
  }
  
  
  /**
  *  接続状態を返す。
  *
  *@return    閉じている場合はTrue。
  */
  public boolean isClosed() {
    try {
      return (con == null || con.isClosed());
    } catch (Exception e) {
      this.outputException(e);
      return true;
    }
    
  }
  
  
  /**
  * ロールバック処理をする
  *
  *@return    Description of the Return Value
  */
  public boolean rollback() {
    if (isClosed()) {
      return false;
    }
    
    try {
      con.rollback();
      return true;
    } catch (Exception e) {
      this.outputException(e);
      return false;
    }
  }
  
  /**
  * 指定された Exceptionクラス インスタンスよりエラーを出力します。
  *
  *@param  e  Exceptionクラス インスタンス
  */
  private void outputException(Exception e){
    switch (errorLog) {
    case TO_LOG:
      //Log.logStackTrace(e);
      break;
    case TO_CONSOLE:
      System.out.println(e);
      break;
    case TO_NO:
      break;
    case TO_DIALOG:
      javax.swing.JOptionPane.showMessageDialog(null, e);
      break;
    }
  }
  
  /**
  * 指定されたエラーの出力先をセットします。
  *
  *@param  errorLog  エラーの出力先。TO_NO、TO_CONSOLE、TO_LOG、TO_DIALOGのいずれか
  */
  public void setErrorLog(int errorLog){
    this.errorLog = errorLog;
  }
  
  /**
  * データベースのコネクションを返します。 
  * 
  *@return    データベースコネクション
  */
  public Connection getConnection() throws SQLException{
    if (connect()){
      return con;
    }
    return null;
  }
  
  /**
  * パラメータの型に合わせて PreparedStatement にパラメータをセットする。
  * 
  *@param  st             PreparedStatementインスタンス
  *@param  param          パラメータのArrayList
  *@exception  Exception  処理中に発生した例外
  */
  public void setParameters(PreparedStatement st, ArrayList<Object> param) throws Exception {
    setParameters(st, param.toArray());
  }
  
  /**
  * パラメータの型に合わせて PreparedStatement にパラメータをセットする。
  * 
  *@param  st             PreparedStatementインスタンス
  *@param  param          パラメータのObject配列
  *@exception  Exception  処理中に発生した例外
  */
  public void setParameters(PreparedStatement st, Object[] param) throws Exception {
    for (int i = 0; i < param.length; i++) {
      if (param[i] instanceof Long){
        st.setLong(i + 1, (Long)param[i]);
      } else if (param[i] instanceof Double){
        st.setDouble(i + 1, (Double)param[i]);
      } else if (param[i] instanceof Integer){
        st.setInt(i + 1, (Integer)param[i]);
      } else if (param[i] instanceof java.sql.Date){
        st.setDate(i + 1, (java.sql.Date)param[i]);
      } else {
        st.setString(i + 1, (String)param[i]);
      }
    }
  }
  
  /** 
  * 指定されたSQLを実行し、実行結果を ArrayList で返す。
  * 
  * @param sql 実行する SQL文
  * @param mode 戻り値のモード。ALL の場合は全件、ONE の場合は最初の１件を返す。
  */
  public ArrayList<HashMap> executeQuery(String sql, MODE mode) throws Exception {
    if (!connect()){
      return null;
    }
    
    PreparedStatement st = con.prepareStatement(sql);
    ArrayList<Object> args = new ArrayList<Object>();
    return executeQuery(st,args, mode);
  }
  
  /** 
  * 指定された PreparedStatement を実行し、実行結果を ArrayList で返す。
  * 
  * @param st 実行する PreparedStatement
  * @param args PreparedStatement の引数。必要ない場合は null。 
  * @param mode 戻り値のモード。ALL の場合は全件、ONE の場合は最初の１件を返す。
  */
  public ArrayList<HashMap> executeQuery(PreparedStatement st, ArrayList<Object> args, MODE mode) throws Exception {
    ResultSet rs = null;
    ArrayList<HashMap> rows = null;
    
    try {
      if (args == null){
        args = new ArrayList<Object>();
      }
      
      
      ResultSetMetaData rsmd = null;
      setParameters(st,args);
      
      rs = st.executeQuery();
      rsmd = rs.getMetaData();
      
      int colCnt = rsmd.getColumnCount();
      String colLabel = "";
      rows = new ArrayList<HashMap>();
      while(rs.next()){
        HashMap<Object,Object> row = new HashMap<Object,Object>();
        for (int i = 0; i < colCnt; i++){
          colLabel = rsmd.getColumnLabel(i+1);
          row.put(colLabel,rs.getObject(colLabel));
        }
        rows.add(row);
        
        if (mode == MODE.ONE){
          break;
        }
      }
      return rows;
    } finally {
      close(st,rs);
    }
  }
}

