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.util.concurrent.TimeUnit;
016import java.time.Duration;
017
018import com.typesafe.config.Config;
019import com.typesafe.config.ConfigObject;
020import com.typesafe.config.ConfigList;
021import com.typesafe.config.ConfigException;
022import com.typesafe.config.ConfigMemorySize;
023import com.typesafe.config.ConfigValue;
024import com.typesafe.config.ConfigValueType;
025
026/**
027 * Internal implementation detail, not ABI stable, do not touch.
028 * For use only by the {@link com.typesafe.config} package.
029 */
030public class ConfigBeanImpl {
031
032    /**
033     * This is public ONLY for use by the "config" package, DO NOT USE this ABI
034     * may change.
035     * @param <T> type of the bean
036     * @param config config to use
037     * @param clazz class of the bean
038     * @return the bean instance
039     */
040    public static <T> T createInternal(Config config, Class<T> clazz) {
041        if (((SimpleConfig)config).root().resolveStatus() != ResolveStatus.RESOLVED)
042            throw new ConfigException.NotResolved(
043                    "need to Config#resolve() a config before using it to initialize a bean, see the API docs for Config#resolve()");
044
045        Map<String, AbstractConfigValue> configProps = new HashMap<String, AbstractConfigValue>();
046        Map<String, String> originalNames = new HashMap<String, String>();
047        for (Map.Entry<String, ConfigValue> configProp : config.root().entrySet()) {
048            String originalName = configProp.getKey();
049            String camelName = ConfigImplUtil.toCamelCase(originalName);
050            // if a setting is in there both as some hyphen name and the camel name,
051            // the camel one wins
052            if (originalNames.containsKey(camelName) && originalName != camelName) {
053                // if we aren't a camel name to start with, we lose.
054                // if we are or we are the first matching key, we win.
055            } else {
056                configProps.put(camelName, (AbstractConfigValue) configProp.getValue());
057                originalNames.put(camelName, originalName);
058            }
059        }
060
061        BeanInfo beanInfo = null;
062        try {
063            beanInfo = Introspector.getBeanInfo(clazz);
064        } catch (IntrospectionException e) {
065            throw new ConfigException.BadBean("Could not get bean information for class " + clazz.getName(), e);
066        }
067
068        try {
069            List<PropertyDescriptor> beanProps = new ArrayList<PropertyDescriptor>();
070            for (PropertyDescriptor beanProp : beanInfo.getPropertyDescriptors()) {
071                if (beanProp.getReadMethod() == null || beanProp.getWriteMethod() == null) {
072                    continue;
073                }
074                beanProps.add(beanProp);
075            }
076
077            // Try to throw all validation issues at once (this does not comprehensively
078            // find every issue, but it should find common ones).
079            List<ConfigException.ValidationProblem> problems = new ArrayList<ConfigException.ValidationProblem>();
080            for (PropertyDescriptor beanProp : beanProps) {
081                Method setter = beanProp.getWriteMethod();
082                Class parameterClass = setter.getParameterTypes()[0];
083
084                ConfigValueType expectedType = getValueTypeOrNull(parameterClass);
085                if (expectedType != null) {
086                    String name = originalNames.get(beanProp.getName());
087                    if (name == null)
088                        name = beanProp.getName();
089                    Path path = Path.newKey(name);
090                    AbstractConfigValue configValue = configProps.get(beanProp.getName());
091                    if (configValue != null) {
092                        SimpleConfig.checkValid(path, expectedType, configValue, problems);
093                    } else {
094                        SimpleConfig.addMissing(problems, expectedType, path, config.origin());
095                    }
096                }
097            }
098
099            if (!problems.isEmpty()) {
100                throw new ConfigException.ValidationFailed(problems);
101            }
102
103            // Fill in the bean instance
104            T bean = clazz.newInstance();
105            for (PropertyDescriptor beanProp : beanProps) {
106                Method setter = beanProp.getWriteMethod();
107                Type parameterType = setter.getGenericParameterTypes()[0];
108                Class parameterClass = setter.getParameterTypes()[0];
109                Object unwrapped = getValue(clazz, parameterType, parameterClass, config, originalNames.get(beanProp.getName()));
110                setter.invoke(bean, unwrapped);
111            }
112            return bean;
113        } catch (InstantiationException e) {
114            throw new ConfigException.BadBean(clazz.getName() + " needs a public no-args constructor to be used as a bean", e);
115        } catch (IllegalAccessException e) {
116            throw new ConfigException.BadBean(clazz.getName() + " getters and setters are not accessible, they must be for use as a bean", e);
117        } catch (InvocationTargetException e) {
118            throw new ConfigException.BadBean("Calling bean method on " + clazz.getName() + " caused an exception", e);
119        }
120    }
121
122    // we could magically make this work in many cases by doing
123    // getAnyRef() (or getValue().unwrapped()), but anytime we
124    // rely on that, we aren't doing the type conversions Config
125    // usually does, and we will throw ClassCastException instead
126    // of a nicer error message giving the name of the bad
127    // setting. So, instead, we only support a limited number of
128    // types plus you can always use Object, ConfigValue, Config,
129    // ConfigObject, etc.  as an escape hatch.
130    private static Object getValue(Class beanClass, Type parameterType, Class parameterClass, Config config, 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}