    001package com.typesafe.config.impl;
    002
    003import java.beans.BeanInfo;
    004import java.beans.IntrospectionException;
    005import java.beans.Introspector;
    006import java.beans.PropertyDescriptor;
    007import java.lang.reflect.InvocationTargetException;
    008import java.lang.reflect.Method;
    009import java.lang.reflect.ParameterizedType;
    010import java.lang.reflect.Type;
    011import java.util.ArrayList;
    012import java.util.HashMap;
    013import java.util.List;
    014import java.util.Map;
    015import java.time.Duration;
    016
    017import com.typesafe.config.Config;
    018import com.typesafe.config.ConfigObject;
    019import com.typesafe.config.ConfigList;
    020import com.typesafe.config.ConfigException;
    021import com.typesafe.config.ConfigMemorySize;
    022import com.typesafe.config.ConfigValue;
    023import com.typesafe.config.ConfigValueType;
    024
    025/**
    026 * Internal implementation detail, not ABI stable, do not touch.
    027 * For use only by the {@link com.typesafe.config} package.
    028 */
    029public class ConfigBeanImpl {
    030
    031    /**
    032     * This is public ONLY for use by the "config" package, DO NOT USE this ABI
    033     * may change.
    034     * @param <T> type of the bean
    035     * @param config config to use
    036     * @param clazz class of the bean
    037     * @return the bean instance
    038     */
    039    public static <T> T createInternal(Config config, Class<T> clazz) {
    040        if (((SimpleConfig)config).root().resolveStatus() != ResolveStatus.RESOLVED)
    041            throw new ConfigException.NotResolved(
    042                    "need to Config#resolve() a config before using it to initialize a bean, see the API docs for Config#resolve()");
    043
    044        Map<String, AbstractConfigValue> configProps = new HashMap<String, AbstractConfigValue>();
    045        Map<String, String> originalNames = new HashMap<String, String>();
    046        for (Map.Entry<String, ConfigValue> configProp : config.root().entrySet()) {
    047            String originalName = configProp.getKey();
    048            String camelName = ConfigImplUtil.toCamelCase(originalName);
    049            // if a setting is in there both as some hyphen name and the camel name,
    050            // the camel one wins
    051            if (originalNames.containsKey(camelName) && !originalName.equals(camelName)) {
    052                // if we aren't a camel name to start with, we lose.
    053                // if we are or we are the first matching key, we win.
    054            } else {
    055                configProps.put(camelName, (AbstractConfigValue) configProp.getValue());
    056                originalNames.put(camelName, originalName);
    057            }
    058        }
    059
    060        BeanInfo beanInfo = null;
    061        try {
    062            beanInfo = Introspector.getBeanInfo(clazz);
    063        } catch (IntrospectionException e) {
    064            throw new ConfigException.BadBean("Could not get bean information for class " + clazz.getName(), e);
    065        }
    066
    067        try {
    068            List<PropertyDescriptor> beanProps = new ArrayList<PropertyDescriptor>();
    069            for (PropertyDescriptor beanProp : beanInfo.getPropertyDescriptors()) {
    070                if (beanProp.getReadMethod() == null || beanProp.getWriteMethod() == null) {
    071                    continue;
    072                }
    073                beanProps.add(beanProp);
    074            }
    075
    076            // Try to throw all validation issues at once (this does not comprehensively
    077            // find every issue, but it should find common ones).
    078            List<ConfigException.ValidationProblem> problems = new ArrayList<ConfigException.ValidationProblem>();
    079            for (PropertyDescriptor beanProp : beanProps) {
    080                Method setter = beanProp.getWriteMethod();
    081                Class<?> parameterClass = setter.getParameterTypes()[0];
    082
    083                ConfigValueType expectedType = getValueTypeOrNull(parameterClass);
    084                if (expectedType != null) {
    085                    String name = originalNames.get(beanProp.getName());
    086                    if (name == null)
    087                        name = beanProp.getName();
    088                    Path path = Path.newKey(name);
    089                    AbstractConfigValue configValue = configProps.get(beanProp.getName());
    090                    if (configValue != null) {
    091                        SimpleConfig.checkValid(path, expectedType, configValue, problems);
    092                    } else {
    093                        SimpleConfig.addMissing(problems, expectedType, path, config.origin());
    094                    }
    095                }
    096            }
    097
    098            if (!problems.isEmpty()) {
    099                throw new ConfigException.ValidationFailed(problems);
    100            }
    101
    102            // Fill in the bean instance
    103            T bean = clazz.newInstance();
    104            for (PropertyDescriptor beanProp : beanProps) {
    105                Method setter = beanProp.getWriteMethod();
    106                Type parameterType = setter.getGenericParameterTypes()[0];
    107                Class<?> parameterClass = setter.getParameterTypes()[0];
    108                Object unwrapped = getValue(clazz, parameterType, parameterClass, config, originalNames.get(beanProp.getName()));
    109                setter.invoke(bean, unwrapped);
    110            }
    111            return bean;
    112        } catch (InstantiationException e) {
    113            throw new ConfigException.BadBean(clazz.getName() + " needs a public no-args constructor to be used as a bean", e);
    114        } catch (IllegalAccessException e) {
    115            throw new ConfigException.BadBean(clazz.getName() + " getters and setters are not accessible, they must be for use as a bean", e);
    116        } catch (InvocationTargetException e) {
    117            throw new ConfigException.BadBean("Calling bean method on " + clazz.getName() + " caused an exception", e);
    118        }
    119    }
    120
    121    // we could magically make this work in many cases by doing
    122    // getAnyRef() (or getValue().unwrapped()), but anytime we
    123    // rely on that, we aren't doing the type conversions Config
    124    // usually does, and we will throw ClassCastException instead
    125    // of a nicer error message giving the name of the bad
    126    // setting. So, instead, we only support a limited number of
    127    // types plus you can always use Object, ConfigValue, Config,
    128    // ConfigObject, etc.  as an escape hatch.
    129    private static Object getValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config,
    130            String configPropName) {
    131        if (parameterClass == Boolean.class || parameterClass == boolean.class) {
    132            return config.getBoolean(configPropName);
    133        } else if (parameterClass == Integer.class || parameterClass == int.class) {
    134            return config.getInt(configPropName);
    135        } else if (parameterClass == Double.class || parameterClass == double.class) {
    136            return config.getDouble(configPropName);
    137        } else if (parameterClass == Long.class || parameterClass == long.class) {
    138            return config.getLong(configPropName);
    139        } else if (parameterClass == String.class) {
    140            return config.getString(configPropName);
    141        } else if (parameterClass == Duration.class) {
    142            return config.getDuration(configPropName);
    143        } else if (parameterClass == ConfigMemorySize.class) {
    144            return config.getMemorySize(configPropName);
    145        } else if (parameterClass == Object.class) {
    146            return config.getAnyRef(configPropName);
    147        } else if (parameterClass == List.class) {
    148            return getListValue(beanClass, parameterType, parameterClass, config, configPropName);
    149        } else if (parameterClass == Map.class) {
    150            // we could do better here, but right now we don't.
    151            Type[] typeArgs = ((ParameterizedType)parameterType).getActualTypeArguments();
    152            if (typeArgs[0] != String.class || typeArgs[1] != Object.class) {
    153                throw new ConfigException.BadBean("Bean property '" + configPropName + "' of class " + beanClass.getName() + " has unsupported Map<" + typeArgs[0] + "," + typeArgs[1] + ">, only Map<String,Object> is supported right now");
    154            }
    155            return config.getObject(configPropName).unwrapped();
    156        } else if (parameterClass == Config.class) {
    157            return config.getConfig(configPropName);
    158        } else if (parameterClass == ConfigObject.class) {
    159            return config.getObject(configPropName);
    160        } else if (parameterClass == ConfigValue.class) {
    161            return config.getValue(configPropName);
    162        } else if (parameterClass == ConfigList.class) {
    163            return config.getList(configPropName);
    164        } else if (hasAtLeastOneBeanProperty(parameterClass)) {
    165            return createInternal(config.getConfig(configPropName), parameterClass);
    166        } else {
    167            throw new ConfigException.BadBean("Bean property " + configPropName + " of class " + beanClass.getName() + " has unsupported type " + parameterType);
    168        }
    169    }
    170
    171    private static Object getListValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPropName) {
    172        Type elementType = ((ParameterizedType)parameterType).getActualTypeArguments()[0];
    173
    174        if (elementType == Boolean.class) {
    175            return config.getBooleanList(configPropName);
    176        } else if (elementType == Integer.class) {
    177            return config.getIntList(configPropName);
    178        } else if (elementType == Double.class) {
    179            return config.getDoubleList(configPropName);
    180        } else if (elementType == Long.class) {
    181            return config.getLongList(configPropName);
    182        } else if (elementType == String.class) {
    183            return config.getStringList(configPropName);
    184        } else if (elementType == Duration.class) {
    185            return config.getDurationList(configPropName);
    186        } else if (elementType == ConfigMemorySize.class) {
    187            return config.getMemorySizeList(configPropName);
    188        } else if (elementType == Object.class) {
    189            return config.getAnyRefList(configPropName);
    190        } else if (elementType == Config.class) {
    191            return config.getConfigList(configPropName);
    192        } else if (elementType == ConfigObject.class) {
    193            return config.getObjectList(configPropName);
    194        } else if (elementType == ConfigValue.class) {
    195            return config.getList(configPropName);
    196        } else {
    197            throw new ConfigException.BadBean("Bean property '" + configPropName + "' of class " + beanClass.getName() + " has unsupported list element type " + elementType);
    198        }
    199    }
    200
    201    // null if we can't easily say; this is heuristic/best-effort
    202    private static ConfigValueType getValueTypeOrNull(Class<?> parameterClass) {
    203        if (parameterClass == Boolean.class || parameterClass == boolean.class) {
    204            return ConfigValueType.BOOLEAN;
    205        } else if (parameterClass == Integer.class || parameterClass == int.class) {
    206            return ConfigValueType.NUMBER;
    207        } else if (parameterClass == Double.class || parameterClass == double.class) {
    208            return ConfigValueType.NUMBER;
    209        } else if (parameterClass == Long.class || parameterClass == long.class) {
    210            return ConfigValueType.NUMBER;
    211        } else if (parameterClass == String.class) {
    212            return ConfigValueType.STRING;
    213        } else if (parameterClass == Duration.class) {
    214            return null;
    215        } else if (parameterClass == ConfigMemorySize.class) {
    216            return null;
    217        } else if (parameterClass == List.class) {
    218            return ConfigValueType.LIST;
    219        } else if (parameterClass == Map.class) {
    220            return ConfigValueType.OBJECT;
    221        } else if (parameterClass == Config.class) {
    222            return ConfigValueType.OBJECT;
    223        } else if (parameterClass == ConfigObject.class) {
    224            return ConfigValueType.OBJECT;
    225        } else if (parameterClass == ConfigList.class) {
    226            return ConfigValueType.LIST;
    227        } else {
    228            return null;
    229        }
    230    }
    231
    232    private static boolean hasAtLeastOneBeanProperty(Class<?> clazz) {
    233        BeanInfo beanInfo = null;
    234        try {
    235            beanInfo = Introspector.getBeanInfo(clazz);
    236        } catch (IntrospectionException e) {
    237            return false;
    238        }
    239
    240        for (PropertyDescriptor beanProp : beanInfo.getPropertyDescriptors()) {
    241            if (beanProp.getReadMethod() != null && beanProp.getWriteMethod() != null) {
    242                return true;
    243            }
    244        }
    245
    246        return false;
    247    }
    248}