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