/*
 * SqlMapper.java
 *
 * Created on 2007/04/12, 16:18
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.itscool.commons.dao;

import java.io.InputStream;
import java.io.Reader;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.itscool.commons.bean.BeanUtil;
import org.itscool.commons.connection.*;
import org.itscool.commons.util.StringUtil;

/**
 * PraparedStatementのラッパークラスです。<br>
 * O/Rマッピング用のメソッドも実装しています。
 * @author kanou
 */
public class PreparedStatementWrapper {
    /** マッピング種別(テーブル名・テーブル定義とクラスでマッピング) */
    public static final int MAP_KIND_TABLEMANE_IS_CLASS = 0;
    /** マッピング種別(クエリーの結果を単一のクラスでマッピング) */
    public static final int MAP_KIND_QUERY_ONE_CLASS = 1;
    /** マッピング種別(クエリーのAS句とクラスでマッピング */
    public static final int MAP_KIND_ASNAME_IS_CLASS = 2;
    
    private PreparedStatement pstmt;
    
    public PreparedStatementWrapper(PreparedStatement pstmt) {
        //TransactionUtil経由で取得したPreparedStatementインスタンスは
        //Connection.close()のタイミングで自動的にクローズされる
        this.pstmt = pstmt;
    }
    
    public void setValue(int id, String value){
        try{
            pstmt.setString(id, value);
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    public void setValue(int id, byte value){
        try{
            pstmt.setByte(id, value);
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    public void setValue(int id, short value){
        try{
            pstmt.setShort(id, value);
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    public void setValue(int id, int value){
        try{
            pstmt.setInt(id, value);
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    public void setValue(int id, long value){
        try{
            pstmt.setLong(id, value);
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    public void setValue(int id, double value){
        try{
            pstmt.setDouble(id, value);
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    public void setValue(int id, float value){
        try{
            pstmt.setFloat(id, value);
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    public void setValue(int id, byte[] value){
        try{
            pstmt.setBytes(id, value);
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    public void setValue(int id, InputStream value){
        try{
            pstmt.setBinaryStream(id, value);
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    public void setValue(int id, Blob value){
        try{
            pstmt.setBlob(id, value);
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    public void setValue(int id, Reader value){
        try{
            pstmt.setCharacterStream(id, value);
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    public void setValue(int id, java.util.Date value){
        try{
            java.sql.Date sqlValue = new java.sql.Date(value.getTime());
            pstmt.setDate(id, sqlValue);
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    public void setValue(int id, Object value){
        try{
            pstmt.setObject(id, value);
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    /**
     * PreparedStatementのexecuteQuery()を実行し、結果セットを引数で指定した
     * オブジェクトリストで返します
     * （外部参照は1階層まで、ManyToOne、OneToOneの制限有り）
     */
    public List select(Class entity){
        return select(entity, MAP_KIND_TABLEMANE_IS_CLASS);
    }
    
    /**
     * PreparedStatementのexecuteQuery()を実行し、結果セットを引数で指定した
     * オブジェクトリストで返します
     * 引数 treeがMAP_KIND_QUERY_ONE_CLASSの場合結合テーブルのカラムも
     * entityのプロパティにセットします。
     * 引数 treeがMAP_KIND_TABLEMANE_IS_CLASSの場合、クラスの関連定義と
     * 外部参照テーブルの関連がそれぞれ対応付けられます。
     * 引数 treeがMAP_KIND_ASNAME_IS_CLASSの場合、クエリーのAS句
     * （テーブル名_AS_フィールド名)に対応するクラスのプロパティに対応付けられます。
     * 
     * 引数
     * つまり、entityに指定された外部参照キーを無視して、結合テーブルのカラムを、全て
     * entityのプロパティにマッピングします。
     * entityのプロパティにマッピングする場合、entityのプロパティ名と
     * カラムの別名（AS句）でマッピングされます。
     * trueの場合、entityが参照するオブジェクトを生成し、参照先のオブジェクトに結合テーブルの
     * 結果セットがマッピングされます。
     * （外部参照は1階層まで、ManyToOne、OneToOneの制限有り）
     */
    public List select(Class entity, int tree){
        LinkedList result = new LinkedList();
        ResultSet rs = null;
        try{
            RecordMap gRecordMap = RecordMapFactory.createRecordMap(entity);
            HashMap fkRecordMaps = RecordMapFkFactory.createFkRecordMaps(entity);
            //select文を実行
            rs = pstmt.executeQuery();
            
            String currentTable = null;
            
            // マッピング種別(クエリーの結果を単一のクラスでマッピング)する場合
            if( tree == MAP_KIND_QUERY_ONE_CLASS ) {
                currentTable = gRecordMap.getTableName();
            }
            List recordList = createRecordList(rs, currentTable, tree);
            
            //作成したrecordListをBeanにセット
            for( int i=0; i<recordList.size(); i++){        //レコード単位
                //1レコード取得
                HashMap record = (HashMap)recordList.get(i);
                //自オブジェクトを作成
                Object newBean = createBean(record, gRecordMap, tree);
                if( newBean == null ) continue;
                
                //関連クラスの作成
                Set keySet = fkRecordMaps.keySet();
                Iterator it = keySet.iterator();
                while(it.hasNext()){     //1レコード毎の処理
                    String fkPropertyName = (String)it.next();
                    RecordMap fkRecordMap = (RecordMap)fkRecordMaps.get(fkPropertyName);
                    if( fkRecordMap == null ){
                        continue;
                    }
                    //関連オブジェクトを作成
                    Object fkBean = createBean(record, fkRecordMap, tree);
                    if( fkBean != null ){
                        //fkBeanをセット
                        BeanUtil.setProperty(newBean, fkPropertyName, fkBean);
                    }
                }
                
                
                
                
                HashMap oneToManyRecordMaps = RecordMapOneToManyFactory.createOneToManyRecordMaps(entity);
                Set mKeySet = oneToManyRecordMaps.keySet();
                Iterator mIt = mKeySet.iterator();
                int childCount=0;
                while(mIt.hasNext()){
                    ArrayList childs = new ArrayList();
                    String propertyName = (String)mIt.next();
                    RecordMap oneToManyRecordMap = (RecordMap)oneToManyRecordMaps.get(propertyName);

                    for( childCount=i;childCount<recordList.size(); childCount++){
                        HashMap child = (HashMap)recordList.get(childCount);
                        Object oneToManyObj = createBean(child, oneToManyRecordMap, tree);
                        childs.add(oneToManyObj);

                        Object chkObj = createBean(child, gRecordMap, tree);
                        if( !RecordMap.isEquals(newBean, chkObj, gRecordMap)){
                            if( childCount > 1 ){
                                childCount --;
                            }
                            break;
                        }
                    }
                    BeanUtil.setProperty(newBean, propertyName, childs);
                }
                if( oneToManyRecordMaps.size() > 0 ){
                    i = childCount;
                }
                
                //オブジェクトを結果リストに登録
                result.add(newBean);
            }
            return result;
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }finally{
            if( rs != null ){
                try{
                    rs.close();
                }catch(Exception e){}
            }
        }
    }
    
    /**
     * ResultSetインスタンスからレコードを取得し、
     * 引数currentTableがnullの場合は、テーブルごとの結果セットをListで返します<br>
     * currentTableがセットされている場合は、全ての結果セットが、currentTable
     * が示すレコードとして作成されます。
     * <pre>
     * List recordList 全レコード
     *   |
     *   +-- HashMap record １レコード（キー：テーブル名）
     *          |
     *          +-- HashMap datasOfTable 1レコードの結果セットをテーブル単位で保持
     *              （キー：カラム名、値：オブジェクト
     * </pre>
     * @param rs ResultSetインスタンス
     * @param currentTable 結果セットを指定されたテーブル名で集約します
     * @return List 結果セットをListで返します
     */
    private List createRecordList(ResultSet rs, String currentTable, int tree) throws SQLException {
        ArrayList recordList = new ArrayList();
        while ( rs.next() ) {
            ResultSetMetaData metaData = rs.getMetaData();
            int columnCount = metaData.getColumnCount();
            HashMap record = new HashMap();
            for(int i=0;i<columnCount;i++){
                String colName = metaData.getColumnName(i+1);
                String colAsName = metaData.getColumnLabel(i+1);
                String tableName = metaData.getTableName(i+1);
                if( currentTable != null ){
                    tableName = currentTable;
                }
                //ResultSetからデータを取得
                Object value = rs.getObject(i+1);
                if( tree == MAP_KIND_ASNAME_IS_CLASS ){     //クエリーのAS句とクラスでマッピング
                    int tableIndex = colAsName.indexOf("_AS_");
                    if( tableIndex >= 0 ){
                        tableName = colAsName.substring(0, tableIndex);
                    }
                    
                    int colNameIndex = tableIndex + ("_AS_").length();
                    if( colAsName.length() > colNameIndex && tableIndex >= 0 ){
                        colAsName = colAsName.substring(colNameIndex, colAsName.length());
                    }
                }
                //1行辺りのテーブルに関連するデータマップ
                HashMap datasOfTable = (HashMap)record.get(tableName);
                if( datasOfTable == null ){
                    //System.out.println("NEW DATAS_OF_TABLE[" + tableName + "]");
                    datasOfTable = new HashMap();
                    record.put(tableName, datasOfTable);
                }
                //System.out.println("TABLE[" + tableName + "], COLUM["+colAsName +"]");
                
                datasOfTable.put(colAsName, value);
            }
            recordList.add(record);
        }
        return recordList;
    }
    
    /**
     * 結果セットに対応するオブジェクトを生成します
     * @param record 1レコードの結果セット
     * @param recordMap クラスとテーブルの関連情報
     * @return 結果セットに対応するオブジェクト
     */
    private Object createBean(HashMap record, RecordMap recordMap, int tree){
        String entityName = recordMap.getClassName();
        String tableName = recordMap.getTableName();
        HashMap datasOfTable = (HashMap)record.get(tableName);
        if( datasOfTable == null ){
            return null;
        }
        Object newBean = BeanUtil.createInstance(entityName);
        Set datasKeySet = datasOfTable.keySet();
        Iterator datasIt = datasKeySet.iterator();
        while(datasIt.hasNext()){
            String colAsName = (String)datasIt.next();
            //オブジェクトの各メンバにselectの結果をセットしていく
            Object value = datasOfTable.get(colAsName);
            
            //カラム名からプロパティ名取得
            //String propertyName = StringUtil.cnvUnderScorNameToLowerName(colAsName);
            FieldMap field = recordMap.getFieldByFieldName(colAsName);
            String propertyName = field.getPropertyName();
            if( value instanceof java.sql.Date ){
                value = new Date(((java.sql.Date)value).getTime());
            }else if( value instanceof Timestamp ){
                Timestamp ts = (Timestamp)value;
                value = new Date(ts.getTime());
            }else if( value instanceof Blob){
                Blob blob = (Blob)value;
                Class type = BeanUtil.getReturnType(newBean, propertyName);
                if( type.isAssignableFrom(InputStream.class)){
                    try{
                        value = (InputStream)blob.getBinaryStream();
                    }catch(SQLException se){}
                }else if( type.isAssignableFrom(byte[].class )){
                    try{
                        value = (byte[])blob.getBytes((long)0, (int)blob.length());
                    }catch(SQLException se){}
                }
            }else if( value instanceof Clob ){
                Clob clob = (Clob)value;
                Class type = BeanUtil.getReturnType(newBean, propertyName);
                if( type.isAssignableFrom(Reader.class)){
                    try{
                        value = (Reader)clob.getCharacterStream();
                    }catch(SQLException se){}
                }else if( type.isAssignableFrom(String.class )){
                    try{
                        value = (String)clob.getSubString((long)0, (int)clob.length());
                    }catch(SQLException se){}
                }
                
            }
            
            BeanUtil.setProperty(newBean, propertyName, value);
        }
        return newBean;
    }
    
    /**
     * PremeredStatement.executeQuery()の実行結果をRecordsインスタンス
     * で取得します。<br>
     * <pre>
     *    Records 全レコード
     *       |
     *       + Record（HashMap) 1レコード分の情報
     *           （キー："テーブル名.カラム名(大文字)"、値：データ)
     * </pre>
     */
    public Records select(){
        ResultSet rs = null;
        try{
            rs = executeQuery();
            ResultSetMetaData metaData = rs.getMetaData();
            int columnCount = metaData.getColumnCount();
            Records records = new Records();
            while( rs.next() ){
                Record record = new Record();
                //結果レコードの集合をHashMapで作成
                for(int i=0;i<columnCount;i++){
                    String colName = metaData.getColumnName(i+1);
                    String colAsName = metaData.getColumnLabel(i+1);
                    String tableName = metaData.getTableName(i+1);
                    
                    //列名に別名が指定されている場合は別名を使用する
                    //集計関数の場合は必ず別名が指定するべきである
                    if(!colAsName.equals("")){
                        colName = colAsName;
                    }
                    colName = colName.toUpperCase();
                    Object value = rs.getObject(i+1);
                    
                    //キー"テーブル名.カラム名"でデータをセット
                    String key;
                    if( tableName != null && tableName.length() > 0 ){
                        key = tableName + "." + colName;
                    }else{
                        key = colName;
                    }
                    key = key.toUpperCase();
                    record.put(key, value);
                    //record.put(colName, value);
                }
                records.add(record);
            }
            return records;
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }finally{
            if( rs != null ){
                try{
                    rs.close();
                }catch(Exception e){}
            }
        }
    }
    
    /**
     * PraperdStatementのexecuteQueryeを実行します
     * @retrun ResultSetインスタンスを返します
     */
    public ResultSet executeQuery(){
        try{
            ResultSet ret = pstmt.executeQuery();
            return ret;
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
    /**
     * PraperdStatementのexecuteUpdateを実行します
     * @return 処理件数を返します
     */
    public int executeUpdate(){
        try{
            int ret = pstmt.executeUpdate();
            return ret;
        }catch(SQLException e){
            throw new TransactionException(e.getMessage());
        }
    }
    
}
