/*
 * $Header: /home/cvs/commons/dbutils-1.0/ja/src/org/apache/commons/dbutils/BasicRowProcessor.java,v 1.1.1.1 2004/02/13 10:02:04 hioki Exp $
 * $Revision: 1.1.1.1 $
 * $Date: 2004/02/13 10:02:04 $
 * 
 * ====================================================================
 *
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2002-2003 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowledgement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgement may appear in the software itself,
 *    if and wherever such third-party acknowledgements normally appear.
 *
 * 4. The names "The Jakarta Project", "Commons", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.commons.dbutils;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * <code>RowProcessor</code> インターフェースの基本実装です。
 *  このクラスはスレッドセーフなSingletonです。 
 * {@primary Basic implementation of the <code>RowProcessor</code> interface.
 * This class is a thread-safe Singleton.}
 * 
 * @see RowProcessor
 * 
 * @author Henri Yandell
 * @author Juozas Baliuka
 * @author David Graham
 * @author Yoav Shapira
 * @translator 小川 環
 * @editor 日置 聡
 * @status underproof 
 * @update 2003/12/19
 */
public class BasicRowProcessor implements RowProcessor {

    /**
     * Set a bean's primitive properties to these defaults when SQL NULL 
     * is returned.  These are the same as the defaults that ResultSet get* 
     * methods return in the event of a NULL column.
     */
    private static final Map primitiveDefaults = new HashMap();

    static {
        primitiveDefaults.put(Integer.TYPE, new Integer(0));
        primitiveDefaults.put(Short.TYPE, new Short((short) 0));
        primitiveDefaults.put(Byte.TYPE, new Byte((byte) 0));
        primitiveDefaults.put(Float.TYPE, new Float(0));
        primitiveDefaults.put(Double.TYPE, new Double(0));
        primitiveDefaults.put(Long.TYPE, new Long(0));
        primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE);
        primitiveDefaults.put(Character.TYPE, new Character('\u0000'));
    }

    /**
     * Special array index that indicates there is no bean property that
     * matches a column from a ResultSet.
     */
    private static final int PROPERTY_NOT_FOUND = -1;

    /**
     * The Singleton instance of this class.
     */
    private static final BasicRowProcessor instance = new BasicRowProcessor();

    /**
     * このクラスのSingletonインスタンスを返します。
     * {@primary Returns the Singleton instance of this class.}
     *
     * @return このクラスの唯一のインスタンス。
     * {@primary The single instance of this class.}
     */
    public static BasicRowProcessor instance() {
        return instance;
    }

    /**
     * BasicRowProcessorのサブクラスのみを対象としたprotectedスコープのコンストラクタ。
     * {@primary Protected constructor for BasicRowProcessor subclasses only.}
     */
    protected BasicRowProcessor() {
        super();
    }

    /**
     * <code>ResultSet</code> の行を <code>Object[]</code> へ変換します。 
     * この実装は、 <code>ResultSet</code> から返されたカラム値を配列へ同順にコピーします。 
     * カラム値がSQL NULLであれば、配列の要素には <code>null</code> がセットされます。
     * {@primary Convert a <code>ResultSet</code> row into an <code>Object[]</code>.
     * This implementation copies column values into the array in the same 
     * order they're returned from the <code>ResultSet</code>.  Array elements
     * will be set to <code>null</code> if the column was SQL NULL.}
     *
     * @see org.apache.commons.dbutils.RowProcessor#toArray(java.sql.ResultSet)
     */
    public Object[] toArray(ResultSet rs) throws SQLException {
        ResultSetMetaData meta = rs.getMetaData();
        int cols = meta.getColumnCount();
        Object[] result = new Object[cols];

        for (int i = 0; i < cols; i++) {
            result[i] = rs.getObject(i + 1);
        }

        return result;
    }

    /**
     * <code>ResultSet</code> の行をJavaBeanへ変換します。 
     * この実装では、カラム名とBeanプロパティ名が一致するかどうか判定するために、 リフレクションおよび
     * <code>BeanInfo</code>のクラスを使用しています。 
     * プロパティはいくつかの要因に基づいたカラムと一致します。 
     * {@primary Convert a <code>ResultSet</code> row into a JavaBean.  This 
     * implementation uses reflection and <code>BeanInfo</code> classes to 
     * match column names to bean property names.  Properties are matched to 
     * columns based on several factors:}
     * <br/>
     * <ol>
     *     <li>
     *     クラスは、カラムと同じ名称の書込可能なプロパティを持っていること。 
     *     名称を比較する際、大小文字を区別しない。
     *     {@primary The class has a writable property with the same name as a column.
     *     The name comparison is case insensitive.}
     *     </li>
     * 
     *     <li>
     *     プロパティのsetメソッドのパラメータの型とカラムの型が一致すること。 
     *     データの型が一致しない場合は、setter メソッドは呼び出されない。
     *     {@primary The property's set method parameter type matches the column 
     *     type. If the data types do not match, the setter will not be called.}
     *     </li>
     * </ol>
     * 
     * <p>
     * <code>ResultSet</code> よりSQL NULLが返されると、 プリミティブ型の
     * Bean プロパティは、その型のデフォルト値がセットされます。 
     * 数値型のフィールドには0がセットされ、boolean 型のフィールドには false がセットされます。 
     * SQL NULLが返されると、オブジェクト型のプロパティには <code>null</code> がセットされます。
     * これは、 <code>ResultSet</code> の get* メソッドと同一の振る舞いです。
     * {@primary Primitive bean properties are set to their defaults when SQL NULL is
     * returned from the <code>ResultSet</code>.  Numeric fields are set to 0
     * and booleans are set to false.  Object bean properties are set to 
     * <code>null</code> when SQL NULL is returned.  This is the same behavior
     * as the <code>ResultSet</code> get* methods.}
     * </p>
     * 
     * @see org.apache.commons.dbutils.RowProcessor#toBean(java.sql.ResultSet, java.lang.Class)
     */
    public Object toBean(ResultSet rs, Class type) throws SQLException {

        PropertyDescriptor[] props = this.propertyDescriptors(type);

        ResultSetMetaData rsmd = rs.getMetaData();

        int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);

        int cols = rsmd.getColumnCount();

        return this.createBean(rs, type, props, columnToProperty, cols);
    }

    /**
     * <code>ResultSet</code> をJavaBeanの <code>List</code> へ変換します。
     * この実装では、カラム名とBeanプロパティ名が一致するかどうか判定するために、 リフレクションおよび
     * <code>BeanInfo</code> のクラスを使用しています。
     * プロパティはいくつかの要因に基づいたカラムと一致します。
     * {@primary Convert a <code>ResultSet</code> into a <code>List</code> of JavaBeans.  
     * This implementation uses reflection and <code>BeanInfo</code> classes to 
     * match column names to bean property names. Properties are matched to 
     * columns based on several factors:}
     * <br/>
     * <ol>
     *     <li>
     *     クラスは、カラムと同じ名称の書込可能なプロパティを持っていること。 
     *     名称を比較する際、大小文字を区別しない。
     *     {@primary The class has a writable property with the same name as a column.
     *     The name comparison is case insensitive.}
     *     </li>
     * 
     *     <li>
     *     プロパティのsetメソッドのパラメータの型とカラムの型が一致すること。 
     *     データの型が一致しない場合は、setter メソッドは呼び出されない。
     *     {@primary The property's set method parameter type matches the column 
     *     type. If the data types do not match, the setter will not be called.}
     *     </li>
     * </ol>
     * 
     * <p>
     * <code>ResultSet</code> よりSQL NULLが返されると、 プリミティブ型の
     * Bean プロパティは、その型のデフォルト値がセットされます。
     * 数値型のフィールドには0がセットされ、boolean 型のフィールドには false がセットされます。
     * SQL NULLが返されると、オブジェクト型のプロパティには <code>null</code> がセットされます。
     * これは、<code>ResultSet</code> の get* メソッドと同一の振る舞いです。
     * {@primary Primitive bean properties are set to their defaults when SQL NULL is
     * returned from the <code>ResultSet</code>.  Numeric fields are set to 0
     * and booleans are set to false.  Object bean properties are set to 
     * <code>null</code> when SQL NULL is returned.  This is the same behavior
     * as the <code>ResultSet</code> get* methods.}
     * </p>
     * 
     * @see org.apache.commons.dbutils.RowProcessor#toBeanList(java.sql.ResultSet, java.lang.Class)
     */
    public List toBeanList(ResultSet rs, Class type) throws SQLException {
        List results = new ArrayList();

        if (!rs.next()) {
            return results;
        }

        PropertyDescriptor[] props = this.propertyDescriptors(type);
        ResultSetMetaData rsmd = rs.getMetaData();
        int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);
        int cols = rsmd.getColumnCount();

        do {
            results.add(this.createBean(rs, type, props, columnToProperty, cols));

        } while (rs.next());

        return results;
    }

    /**
     * Creates a new object and initializes its fields from the ResultSet.
     *
     * @param rs The result set
     * @param type The bean type (the return type of the object)
     * @param props The property descriptors
     * @param columnToProperty The column indices in the result set
     * @param cols The number of columns
     * @return An initialized object.
     * @throws SQLException If a database error occurs
     */
    private Object createBean(
        ResultSet rs,
        Class type,
        PropertyDescriptor[] props,
        int[] columnToProperty,
        int cols)
        throws SQLException {

        Object bean = this.newInstance(type);

        for (int i = 1; i <= cols; i++) {

            if (columnToProperty[i] == PROPERTY_NOT_FOUND) {
                continue;
            }
            
            Object value = rs.getObject(i);

            PropertyDescriptor prop = props[columnToProperty[i]];
            Class propType = prop.getPropertyType();

            if (propType != null && value == null && propType.isPrimitive()) {
                value = primitiveDefaults.get(propType);
            }

            this.callSetter(bean, prop, value);
        }

        return bean;
    }

    /**
     * The positions in the returned array represent column numbers.  The values
     * stored at each position represent the index in the PropertyDescriptor[] 
     * for the bean property that matches the column name.  If no bean property
     * was found for a column, the position is set to PROPERTY_NOT_FOUND.
     * 
     * @param rsmd The result set meta data containing column information
     * @param props The bean property descriptors
     * @return An int[] with column index to property index mappings.  The 0th 
     * element is meaningless as column indexing starts at 1.
     * 
     * @throws SQLException If a database error occurs
     */
    private int[] mapColumnsToProperties(
        ResultSetMetaData rsmd,
        PropertyDescriptor[] props)
        throws SQLException {

        int cols = rsmd.getColumnCount();
        int columnToProperty[] = new int[cols + 1];

        for (int col = 1; col <= cols; col++) {
            String columnName = rsmd.getColumnName(col);
            for (int i = 0; i < props.length; i++) {

                if (columnName.equalsIgnoreCase(props[i].getName())) {
                    columnToProperty[col] = i;
                    break;

                } else {
                    columnToProperty[col] = PROPERTY_NOT_FOUND;
                }
            }
        }

        return columnToProperty;
    }

    /**
     * <code>ResultSet</code> の行を <code>Map</code> へ変換します。
     *  この実装では、大小文字を区別しないカラム名をキーとしたMapを返します。 
     * 例えば、<code>map.get("COL")</code> の呼び出しと
     * <code>map.get("col")</code> の呼び出しでは、 同一の値が返されるようになります。
     * {@primary Convert a <code>ResultSet</code> row into a <code>Map</code>.  This 
     * implementation returns a <code>Map</code> with case insensitive column
     * names as keys.  Calls to <code>map.get("COL")</code> and 
     * <code>map.get("col")</code> return the same value.}
     * @see org.apache.commons.dbutils.RowProcessor#toMap(java.sql.ResultSet)
     */
    public Map toMap(ResultSet rs) throws SQLException {
        Map result = new CaseInsensitiveHashMap();
        ResultSetMetaData rsmd = rs.getMetaData();
        int cols = rsmd.getColumnCount();

        for (int i = 1; i <= cols; i++) {
            result.put(rsmd.getColumnName(i), rs.getObject(i));
        }

        return result;
    }

    /**
     * Calls the setter method on the target object for the given property.
     * If no setter method exists for the property, this method does nothing.
     * @param target The object to set the property on.
     * @param prop The property to set.
     * @param value The value to pass into the setter.
     * @throws SQLException if an error occurs setting the property.
     */
    private void callSetter(
        Object target,
        PropertyDescriptor prop,
        Object value)
        throws SQLException {

        Method setter = prop.getWriteMethod();

        if (setter == null) {
            return;
        }

        Class[] params = setter.getParameterTypes();
        try {
            // Don't call setter if the value object isn't the right type 
            if (this.isCompatibleType(value, params[0])) {
                setter.invoke(target, new Object[] { value });
            }

        } catch (IllegalArgumentException e) {
            throw new SQLException(
                "Cannot set " + prop.getName() + ": " + e.getMessage());

        } catch (IllegalAccessException e) {
            throw new SQLException(
                "Cannot set " + prop.getName() + ": " + e.getMessage());

        } catch (InvocationTargetException e) {
            throw new SQLException(
                "Cannot set " + prop.getName() + ": " + e.getMessage());
        }
    }

    /**
     * ResultSet.getObject() returns an Integer object for an INT column.  The
     * setter method for the property might take an Integer or a primitive int.
     * This method returns true if the value can be successfully passed into
     * the setter method.  Remember, Method.invoke() handles the unwrapping
     * of Integer into an int.
     * 
     * @param value The value to be passed into the setter method.
     * @param type The setter's parameter type.
     * @return boolean True if the value is compatible.
     */
    private boolean isCompatibleType(Object value, Class type) {
        // Do object check first, then primitives
        if (value == null || type.isInstance(value)) {
            return true;

        } else if (
            type.equals(Integer.TYPE) && Integer.class.isInstance(value)) {
            return true;

        } else if (type.equals(Long.TYPE) && Long.class.isInstance(value)) {
            return true;

        } else if (
            type.equals(Double.TYPE) && Double.class.isInstance(value)) {
            return true;

        } else if (type.equals(Float.TYPE) && Float.class.isInstance(value)) {
            return true;

        } else if (type.equals(Short.TYPE) && Short.class.isInstance(value)) {
            return true;

        } else if (type.equals(Byte.TYPE) && Byte.class.isInstance(value)) {
            return true;

        } else if (
            type.equals(Character.TYPE) && Character.class.isInstance(value)) {
            return true;

        } else if (
            type.equals(Boolean.TYPE) && Boolean.class.isInstance(value)) {
            return true;

        } else {
            return false;
        }

    }

    /**
     * Returns a new instance of the given Class.
     *
     * @param c The Class to create an object from.
     * @return A newly created object of the Class.
     * @throws SQLException if creation failed.
     */
    private Object newInstance(Class c) throws SQLException {
        try {
            return c.newInstance();

        } catch (InstantiationException e) {
            throw new SQLException(
                "Cannot create " + c.getName() + ": " + e.getMessage());

        } catch (IllegalAccessException e) {
            throw new SQLException(
                "Cannot create " + c.getName() + ": " + e.getMessage());
        }
    }

    /**
     * Returns a PropertyDescriptor[] for the given Class.
     *
     * @param c The Class to retrieve PropertyDescriptors for.
     * @return A PropertyDescriptor[] describing the Class.
     * @throws SQLException if introspection failed.
     */
    private PropertyDescriptor[] propertyDescriptors(Class c)
        throws SQLException {
        // Introspector caches BeanInfo classes for better performance
        BeanInfo beanInfo = null;
        try {
            beanInfo = Introspector.getBeanInfo(c);

        } catch (IntrospectionException e) {
            throw new SQLException(
                "Bean introspection failed: " + e.getMessage());
        }

        return beanInfo.getPropertyDescriptors();
    }

    /**
     * A Map that converts all keys to lowercase Strings for case insensitive
     * lookups.  This is needed for the toMap() implementation because 
     * databases don't consistenly handle the casing of column names. 
     */
    private static class CaseInsensitiveHashMap extends HashMap {

        /**
         * @see java.util.Map#containsKey(java.lang.Object)
         */
        public boolean containsKey(Object key) {
            return super.containsKey(key.toString().toLowerCase());
        }

        /**
         * @see java.util.Map#get(java.lang.Object)
         */
        public Object get(Object key) {
            return super.get(key.toString().toLowerCase());
        }

        /**
         * @see java.util.Map#put(java.lang.Object, java.lang.Object)
         */
        public Object put(Object key, Object value) {
            return super.put(key.toString().toLowerCase(), value);
        }

        /**
         * @see java.util.Map#putAll(java.util.Map)
         */
        public void putAll(Map m) {
            Iterator iter = m.keySet().iterator();
            while (iter.hasNext()) {
                Object key = iter.next();
                Object value = m.get(key);
                this.put(key, value);
            }
        }

        /**
         * @see java.util.Map#remove(java.lang.ObjecT)
         */
        public Object remove(Object key) {
            return super.remove(key.toString().toLowerCase());
        }
    }
    
}

