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

com.firefly.db.DefaultBeanProcessor Maven / Gradle / Ivy

There is a newer version: 5.0.0-dev6
Show newest version
package com.firefly.db;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.dbutils.BeanProcessor;

import com.firefly.db.annotation.Column;
import com.firefly.db.annotation.Id;
import com.firefly.db.annotation.Table;
import com.firefly.utils.Assert;
import com.firefly.utils.ReflectUtils;
import com.firefly.utils.StringUtils;
import com.firefly.utils.collection.ConcurrentReferenceHashMap;

public class DefaultBeanProcessor extends BeanProcessor {

    private final ConcurrentReferenceHashMap, Map> mapperCache = new ConcurrentReferenceHashMap<>(128);
    private final ConcurrentReferenceHashMap, SQLMapper> insertCache = new ConcurrentReferenceHashMap<>(128);
    private final ConcurrentReferenceHashMap, SQLMapper> queryCache = new ConcurrentReferenceHashMap<>(128);
    private final ConcurrentReferenceHashMap, SQLMapper> deleteCache = new ConcurrentReferenceHashMap<>(128);

    /**
     * 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, 0f);
        primitiveDefaults.put(Double.TYPE, 0d);
        primitiveDefaults.put(Long.TYPE, 0L);
        primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE);
        primitiveDefaults.put(Character.TYPE, (char) 0);
    }

    @Override
    public  T toBean(ResultSet rs, Class type) throws SQLException {
        PropertyDescriptor[] props = this.propertyDescriptors(type);
        ResultSetMetaData rsmd = rs.getMetaData();
        int[] columnToProperty = this.mapColumnsToProperties(rsmd, props, type);
        return this.createBean(rs, type, props, columnToProperty);
    }

    @Override
    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, type);

        do {
            results.add(this.createBean(rs, type, props, columnToProperty));
        } while (rs.next());

        return results;
    }

    public SQLMapper generateDeleteSQL(Class t) {
        return deleteCache.get(t, this::_generateDeleteSQL);
    }

    private SQLMapper _generateDeleteSQL(Class t) {
        SQLMapper sqlMapper = new SQLMapper();
        StringBuilder sql = new StringBuilder();
        String tableName = getTableName(t);
        String catalog = getCatalog(t);
        String idColumnName = getIdColumnName(t);

        sql.append("delete from ");
        if (StringUtils.hasText(catalog)) {
            sql.append(" `").append(catalog).append("`.").append("`").append(tableName).append("` ");
        } else {
            sql.append(" `").append(tableName).append("` ");
        }
        sql.append(" where `").append(idColumnName).append("` = ?");
        sqlMapper.sql = sql.toString();
        return sqlMapper;
    }

    public SQLMapper generateQuerySQL(Class t) {
        return queryCache.get(t, this::_generateQuerySQL);
    }

    private SQLMapper _generateQuerySQL(Class t) {
        SQLMapper sqlMapper = new SQLMapper();
        StringBuilder sql = new StringBuilder();

        sql.append("select");
        Map m = getMapper(t);
        m.forEach((property, mapper) -> {
            if (mapper.annotated) {
                sql.append(" `").append(mapper.columnName).append("`,");
            }
        });
        sql.deleteCharAt(sql.length() - 1);

        String tableName = getTableName(t);
        String catalog = getCatalog(t);
        String idColumnName = getIdColumnName(t);
        sql.append(" from ");
        if (StringUtils.hasText(catalog)) {
            sql.append(" `").append(catalog).append("`.").append("`").append(tableName).append("` ");
        } else {
            sql.append(" `").append(tableName).append("` ");
        }
        sql.append(" where `").append(idColumnName).append("` = ?");

        sqlMapper.sql = sql.toString();
        return sqlMapper;
    }

    public SQLMapper generateUpdateSQL(Class t, Object object) {
        SQLMapper sqlMapper = new SQLMapper();
        StringBuilder sql = new StringBuilder();
        Map m = getMapper(t);
        List mapperList = new ArrayList<>();
        m.forEach((property, mapper) -> {
            if (!mapper.autoIncrement && mapper.annotated) {
                try {
                    Object value = ReflectUtils.get(object, mapper.propertyName);
                    if (value != null) {
                        mapperList.add(mapper);
                    }
                } catch (Throwable ignored) {
                }
            }
        });

        Map propertyMap = new HashMap<>();
        String tableName = getTableName(t);
        String catalog = getCatalog(t);
        Mapper idMapper = getIdMapper(t);
        Assert.notNull(idMapper, "id column must not be null");
        String idColumnName = idMapper.columnName;

        sql.append("update ");
        if (StringUtils.hasText(catalog)) {
            sql.append(" `").append(catalog).append("`.").append("`").append(tableName).append("` ");
        } else {
            sql.append(" `").append(tableName).append("` ");
        }
        sql.append(" set ");
        for (int i = 0; i < mapperList.size(); i++) {
            Mapper mapper = mapperList.get(i);
            if (i == 0) {
                sql.append('`').append(mapper.columnName).append("` = ?");
            } else {
                sql.append(", `").append(mapper.columnName).append("` = ?");
            }
            propertyMap.put(mapper.propertyName, i);
        }
        sql.append(" where `").append(idColumnName).append("` = ?");
        propertyMap.put(idMapper.propertyName, mapperList.size());

        sqlMapper.sql = sql.toString();
        sqlMapper.propertyMap = propertyMap;
        return sqlMapper;
    }

    public SQLMapper generateInsertSQL(Class t) {
        return insertCache.get(t, this::_generateInsertSQL);
    }

    private SQLMapper _generateInsertSQL(Class t) {
        SQLMapper sqlMapper = new SQLMapper();
        StringBuilder sql = new StringBuilder();
        Map m = getMapper(t);
        List mapperList = new ArrayList<>();
        m.forEach((property, mapper) -> {
            if (!mapper.autoIncrement && mapper.annotated) {
                mapperList.add(mapper);
            }
        });

        Map propertyMap = new HashMap<>();
        String tableName = getTableName(t);
        String catalog = getCatalog(t);

        sql.append("insert into ");
        if (StringUtils.hasText(catalog)) {
            sql.append(" `").append(catalog).append("`.").append("`").append(tableName).append("` ");
        } else {
            sql.append(" `").append(tableName).append("` ");
        }
        sql.append(" (");
        for (int i = 0; i < mapperList.size(); i++) {
            Mapper mapper = mapperList.get(i);
            if (i == 0) {
                sql.append('`').append(mapper.columnName).append('`');
            } else {
                sql.append(", `").append(mapper.columnName).append('`');
            }
            propertyMap.put(mapper.propertyName, i);
        }
        sql.append(") values (");
        for (int i = 0; i < mapperList.size(); i++) {
            if (i == 0) {
                sql.append("?");
            } else {
                sql.append(", ?");
            }
        }
        sql.append(")");
        sqlMapper.sql = sql.toString();
        sqlMapper.propertyMap = propertyMap;
        return sqlMapper;
    }

    public static class SQLMapper {
        public String sql;
        public Map propertyMap;

        @Override
        public String toString() {
            return "SQLMapper [sql=" + sql + ", propertyMap=" + propertyMap + "]";
        }

    }

    public String getTableName(Class t) {
        Table table = t.getAnnotation(Table.class);
        if (table != null) {
            return table.value();
        } else {
            return t.getSimpleName();
        }
    }

    public String getCatalog(Class t) {
        Table table = t.getAnnotation(Table.class);
        if (table != null) {
            return table.catalog();
        } else {
            return null;
        }
    }

    public String getIdColumnName(Class t) {
        Mapper mapper = getIdMapper(t);
        if (mapper == null) {
            return null;
        } else {
            return mapper.columnName;
        }
    }

    public Mapper getIdMapper(Class t) {
        Map map = getMapper(t);
        for (Map.Entry entry : map.entrySet()) {
            if (entry.getValue().idColumn) {
                return entry.getValue();
            }
        }
        return null;
    }

    public Map getMapper(Class t) {
        return mapperCache.get(t, this::_getMapper);
    }

    private void idToMapper(Id id, Mapper mapper) {
        if (id != null) {
            if (StringUtils.hasText(id.value())) {
                mapper.columnName = id.value();
            }
            mapper.idColumn = true;
            mapper.autoIncrement = id.autoIncrement();
        }
    }

    private void columnToMapper(Column column, Mapper mapper) {
        if (column != null) {
            if (StringUtils.hasText(column.value())) {
                mapper.columnName = column.value();
            }
        }
    }

    private void annotationToMapper(AnnotatedElement e, Mapper mapper) {
        if (e != null) {
            idToMapper(e.getAnnotation(Id.class), mapper);

            if (mapper.columnName == null) {
                columnToMapper(e.getAnnotation(Column.class), mapper);
            }
        }
    }

    private Map _getMapper(Class t) {
        Map ret = new HashMap<>();
        Map getterMethodMap = ReflectUtils.getGetterMethods(t);
        Map setterMethodMap = ReflectUtils.getSetterMethods(t);
        Set properties = new HashSet<>();

        getterMethodMap.forEach((property, method) -> properties.add(property));
        setterMethodMap.forEach((property, method) -> properties.add(property));

        properties.forEach((property) -> {
            Mapper mapper = new Mapper();
            mapper.propertyName = property;

            try {
                Field field = t.getDeclaredField(property);
                annotationToMapper(field, mapper);
            } catch (Exception ignored) {
            }

            if (mapper.columnName == null) {
                Method getterMethod = getterMethodMap.get(property);
                annotationToMapper(getterMethod, mapper);
            }

            if (mapper.columnName == null) {
                Method setterMethod = setterMethodMap.get(property);
                annotationToMapper(setterMethod, mapper);
            }

            if (mapper.columnName == null) {
                mapper.columnName = property;
            } else {
                mapper.annotated = true;
            }

            ret.put(property, mapper);
        });

        return ret;
    }

    public static class Mapper {
        public String propertyName;
        public String columnName;
        public boolean idColumn;
        public boolean autoIncrement;
        public boolean annotated;

        @Override
        public String toString() {
            return "Mapper [propertyName=" + propertyName + ", columnName=" + columnName + ", idColumn=" + idColumn
                    + ", autoIncrement=" + autoIncrement + ", annotated=" + annotated + "]";
        }

    }

    private int[] mapColumnsToProperties(ResultSetMetaData rsmd, PropertyDescriptor[] props, Class type)
            throws SQLException {
        int cols = rsmd.getColumnCount();
        int[] columnToProperty = new int[cols + 1];
        Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);

        Map map = getMapper(type);

        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++) {
                PropertyDescriptor p = props[i];
                if ("class".equals(p.getName()))
                    continue;

                Mapper mapper = map.get(p.getName());
                if (mapper.annotated) {
                    if (columnName.equalsIgnoreCase(mapper.columnName)) {
                        columnToProperty[col] = i;
                        break;
                    }
                } else {
                    final String generousColumnName = columnName.replace("_", "");

                    if (columnName.equalsIgnoreCase(mapper.columnName)
                            || generousColumnName.equalsIgnoreCase(mapper.columnName)) {
                        columnToProperty[col] = i;
                        break;
                    }
                }
            }
        }

        return columnToProperty;
    }

    /**
     * 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;
        try {
            beanInfo = Introspector.getBeanInfo(c);

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

        return beanInfo.getPropertyDescriptors();
    }

    private  T createBean(ResultSet rs, Class type, PropertyDescriptor[] props, int[] columnToProperty)
            throws SQLException {

        T bean = this.newInstance(type);

        for (int i = 1; i < columnToProperty.length; i++) {

            if (columnToProperty[i] == PROPERTY_NOT_FOUND) {
                continue;
            }

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

            Object value = null;
            if (propType != null) {
                value = this.processColumn(rs, i, propType);

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

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

        return bean;
    }

    /**
     * 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 {
            // convert types for some popular ones
            if (value instanceof java.util.Date) {
                final String targetType = params[0].getName();
                if ("java.sql.Date".equals(targetType)) {
                    value = new java.sql.Date(((java.util.Date) value).getTime());
                } else if ("java.sql.Time".equals(targetType)) {
                    value = new java.sql.Time(((java.util.Date) value).getTime());
                } else if ("java.sql.Timestamp".equals(targetType)) {
                    Timestamp tsValue = (Timestamp) value;
                    int nanos = tsValue.getNanos();
                    value = new java.sql.Timestamp(tsValue.getTime());
                    ((Timestamp) value).setNanos(nanos);
                }
            } else if (value instanceof String && params[0].isEnum()) {
                value = Enum.valueOf(params[0].asSubclass(Enum.class), (String) value);
            }

            // Don't call setter if the value object isn't the right type
            if (this.isCompatibleType(value, params[0])) {
                setter.invoke(target, value);
            } else {
                throw new SQLException("Cannot set " + prop.getName() + ": incompatible types, cannot convert "
                        + value.getClass().getName() + " to " + params[0].getName());
                // value cannot be null here because isCompatibleType allows
                // null
            }

        } catch (IllegalArgumentException | IllegalAccessException | 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 (non-null)
     * @return boolean True if the value is compatible (null => true)
     */
    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) && value instanceof Integer) {
            return true;

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

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

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

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

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

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

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

        }
        return false;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy