All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.arakelian.jdbc.handler.BeanHandler Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.arakelian.jdbc.handler;

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.Date;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import com.arakelian.core.utils.DateUtils;

/**
 * ResultSetHandler implementation that converts the next ResultSet row
 * into a JavaBean.
 *
 * @param 
 *            the type of bean that we return
 */
public class BeanHandler implements ResultSetHandler {
    /**
     * Special array value used by mapColumnsToProperties that indicates there is no
     * bean property that matches a column from a ResultSet.
     */
    protected static final int PROPERTY_NOT_FOUND = -1;

    /**
     * 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, Object> primitiveDefaults = new HashMap<>();

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

    /**
     * The Class of beans produced by this handler.
     */
    private final Class type;

    private PropertyDescriptor[] props = null;

    private int[] columnToProperty = null;

    public BeanHandler(final Class type) {
        this.type = type;
    }

    public int[] getColumnToProperty() {
        return columnToProperty;
    }

    public PropertyDescriptor[] getProps() {
        return props;
    }

    public Class getType() {
        return type;
    }

    /**
     * Convert the first row of the ResultSet into a bean with the Class
     * given in the constructor.
     *
     * @param rs
     *            ResultSet to process.
     * @return An initialized JavaBean or null if there were no rows in the
     *         ResultSet.
     *
     * @throws SQLException
     *             if a database access error occurs
     */
    @Override
    public T handle(final ResultSet rs, final ResultSetMetaData rsmd) throws SQLException {
        if (rs.next()) {
            if (props == null) {
                props = getPropertyDescriptors(type);
            }
            if (columnToProperty == null) {
                columnToProperty = mapColumnsToProperties(rsmd, props);
            }
            return createBean(rs, type, props, columnToProperty);
        } else {
            return null;
        }
    }

    @Override
    public boolean wasLast(final T result) {
        return result == null;
    }

    /**
     * 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(final Object target, final PropertyDescriptor prop, Object value)
            throws SQLException {
        final Method setter = prop.getWriteMethod();
        if (setter == null) {
            return;
        }

        final Class[] params = setter.getParameterTypes();
        try {
            // convert types for some popular ones
            if (value != null) {
                if (value instanceof java.util.Date) {
                    if (params[0].getName().equals("java.sql.Date")) {
                        value = new java.sql.Date(((java.util.Date) value).getTime());
                    } else if (params[0].getName().equals("java.sql.Time")) {
                        value = new java.sql.Time(((java.util.Date) value).getTime());
                    } else if (params[0].getName().equals("java.sql.Timestamp")) {
                        value = new java.sql.Timestamp(((java.util.Date) value).getTime());
                    }
                }
            }

            // Don't call setter if the value object isn't the right type
            if (isCompatibleType(value, params[0])) {
                setter.invoke(target, new Object[] { value });
            } else {
                throw new SQLException("Cannot set " + prop.getName() + ": incompatible types.");
            }

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

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

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

    /**
     * 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.
     * @return An initialized object.
     * @throws SQLException
     *             if a database error occurs.
     */
    private T createBean(
            final ResultSet rs,
            final Class type,
            final PropertyDescriptor[] props,
            final int[] columnToProperty) throws SQLException {
        final T bean = newInstance(type);

        final ResultSetMetaData rsmd = rs.getMetaData();
        for (int column = 1; column < columnToProperty.length; column++) {
            if (columnToProperty[column] == PROPERTY_NOT_FOUND) {
                continue;
            }

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

            Object value = processColumn(rs, rsmd, column, propType);
            if (propType != null && value == null && propType.isPrimitive()) {
                value = primitiveDefaults.get(propType);
            }

            callSetter(bean, prop, value);
        }

        return bean;
    }

    /**
     * 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[] getPropertyDescriptors(final Class c) throws SQLException {
        // Introspector caches BeanInfo classes for better performance
        BeanInfo beanInfo = null;
        try {
            beanInfo = Introspector.getBeanInfo(c);
        } catch (final IntrospectionException e) {
            throw new SQLException("Bean introspection failed: " + e.getMessage());
        }

        return beanInfo.getPropertyDescriptors();
    }

    /**
     * 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(final Object value, final 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;
        }

    }

    /**
     * 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 ResultSetMetaData containing column information.
     *
     * @param props
     *            The bean property descriptors.
     *
     * @throws SQLException
     *             if a database access error occurs
     *
     * @return An int[] with column index to property index mappings. The 0th element is meaningless
     *         because JDBC column indexing starts at 1.
     */
    protected int[] mapColumnsToProperties(final ResultSetMetaData rsmd, final PropertyDescriptor[] props)
            throws SQLException {

        final int cols = rsmd.getColumnCount();
        final int columnToProperty[] = new int[cols + 1];
        Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);

        for (int col = 1; col <= cols; col++) {
            String columnName = rsmd.getColumnLabel(col);
            if (null == columnName || 0 == columnName.length()) {
                columnName = rsmd.getColumnName(col);
            }
            for (int i = 0; i < props.length; i++) {
                if (columnName.equalsIgnoreCase(props[i].getName())) {
                    columnToProperty[col] = i;
                    break;
                }
            }
            if (columnToProperty[col] == PROPERTY_NOT_FOUND) {
                columnName = columnName.replace("_", "");
                for (int i = 0; i < props.length; i++) {
                    if (columnName.equalsIgnoreCase(props[i].getName())) {
                        columnToProperty[col] = i;
                        break;
                    }
                }
            }
        }

        return columnToProperty;
    }

    /**
     * Factory method that returns a new instance of the given Class. This is called at the start of
     * the bean creation process and may be overridden to provide custom behavior like returning a
     * cached bean instance.
     *
     * @param c
     *            The Class to create an object from.
     * @return A newly created object of the Class.
     * @throws SQLException
     *             if creation failed.
     */
    protected T newInstance(final Class c) throws SQLException {
        try {
            return c.getConstructor().newInstance();
        } catch (final InstantiationException | IllegalAccessException | IllegalArgumentException
                | InvocationTargetException | NoSuchMethodException | SecurityException e) {
            throw new SQLException("Cannot create " + c.getName() + ": " + e.getMessage());
        }
    }

    /**
     * Convert a ResultSet column into an object. Simple implementations could just
     * call rs.getObject(index) while more complex implementations could perform type
     * manipulation to match the column's type to the bean property type.
     *
     * 

* This implementation calls the appropriate ResultSet getter method for the given * property type to perform the type conversion. If the property type doesn't match one of the * supported ResultSet types, getObject is called. *

* * @param rs * The ResultSet currently being processed. It is positioned on a valid * row before being passed into this method. * @param rsmd * TODO * @param column * The current column index being processed. * @param propType * The bean property type that this column needs to be converted into. * * @throws SQLException * if a database access error occurs * * @return The object from the ResultSet at the given column index after optional * type processing or null if the column value was SQL NULL. */ protected Object processColumn( final ResultSet rs, final ResultSetMetaData rsmd, final int column, final Class propType) throws SQLException { if (!propType.isPrimitive() && rs.getObject(column) == null) { return null; } if (propType.equals(String.class)) { return rs.getString(column); } else if (propType.equals(Integer.TYPE) || propType.equals(Integer.class)) { return rs.getInt(column); } else if (propType.equals(Boolean.TYPE) || propType.equals(Boolean.class)) { return rs.getBoolean(column); } else if (propType.equals(Long.TYPE) || propType.equals(Long.class)) { return rs.getLong(column); } else if (propType.equals(Double.TYPE) || propType.equals(Double.class)) { return rs.getDouble(column); } else if (propType.equals(Float.TYPE) || propType.equals(Float.class)) { return rs.getFloat(column); } else if (propType.equals(Short.TYPE) || propType.equals(Short.class)) { return rs.getShort(column); } else if (propType.equals(Byte.TYPE) || propType.equals(Byte.class)) { return rs.getByte(column); } else if (propType.equals(Character.TYPE) || propType.equals(Character.class)) { final String s = rs.getString(column); if (s != null && s.length() == 1) { return Character.valueOf(s.charAt(0)); } else { return null; } } else if (propType.equals(Timestamp.class)) { return rs.getTimestamp(column); } else if (propType.equals(ZonedDateTime.class)) { switch (rsmd.getColumnType(column)) { case Types.DATE: return DateUtils.toZonedDateTimeUtc(rs.getDate(column)); case Types.TIMESTAMP: case Types.TIMESTAMP_WITH_TIMEZONE: return DateUtils.toZonedDateTimeUtc(rs.getTimestamp(column)); default: return DateUtils.toZonedDateTimeUtc(rs.getString(column)); } } else if (propType.equals(Date.class)) { return rs.getDate(column); } else { return rs.getObject(column); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy