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, boolean allowUnknownConfigKeys) {
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            Set<String> beanPropNames = new HashSet<String>();
080            for (PropertyDescriptor beanProp : beanProps) {
081                beanPropNames.add(beanProp.getName());
082            }
083
084            // Try to throw all validation issues at once (this does not comprehensively
085            // find every issue, but it should find common ones).
086            List<ConfigException.ValidationProblem> problems = new ArrayList<ConfigException.ValidationProblem>();
087            if (!allowUnknownConfigKeys) {
088                for (Map.Entry<String, String> nameEntry : originalNames.entrySet()) {
089                    String camelName = nameEntry.getKey();
090                    if (!beanPropNames.contains(camelName)) {
091                        AbstractConfigValue configValue = configProps.get(camelName);
092                        problems.add(new ConfigException.ValidationProblem(
093                                Path.newKey(nameEntry.getValue()).render(),
094                                configValue.origin(),
095                                "Unknown config setting"));
096                    }
097                }
098            }
099            for (PropertyDescriptor beanProp : beanProps) {
100                Method setter = beanProp.getWriteMethod();
101                Class<?> parameterClass = setter.getParameterTypes()[0];
102
103                ConfigValueType expectedType = getValueTypeOrNull(parameterClass);
104                if (expectedType != null) {
105                    String name = originalNames.get(beanProp.getName());
106                    if (name == null)
107                        name = beanProp.getName();
108                    Path path = Path.newKey(name);
109                    AbstractConfigValue configValue = configProps.get(beanProp.getName());
110                    if (configValue != null) {
111                        SimpleConfig.checkValid(path, expectedType, configValue, problems);
112                    } else {
113                        if (!isOptionalProperty(clazz, beanProp)) {
114                            SimpleConfig.addMissing(problems, expectedType, path, config.origin());
115                        }
116                    }
117                }
118            }
119
120            if (!problems.isEmpty()) {
121                throw new ConfigException.ValidationFailed(problems);
122            }
123
124            // Fill in the bean instance
125            T bean = clazz.getDeclaredConstructor().newInstance();
126            for (PropertyDescriptor beanProp : beanProps) {
127                Method setter = beanProp.getWriteMethod();
128                Type parameterType = setter.getGenericParameterTypes()[0];
129                Class<?> parameterClass = setter.getParameterTypes()[0];
130                String configPropName = originalNames.get(beanProp.getName());
131                // Is the property key missing in the config?
132                if (configPropName == null) {
133                    // If so, continue if the field is marked as @{link Optional}
134                    if (isOptionalProperty(clazz, beanProp)) {
135                        continue;
136                    }
137                    // Otherwise, raise a {@link Missing} exception right here
138                    throw new ConfigException.Missing(beanProp.getName());
139                }
140                Object unwrapped = getValue(clazz, parameterType, parameterClass, config, configPropName,
141                        allowUnknownConfigKeys);
142                setter.invoke(bean, unwrapped);
143            }
144            return bean;
145        } catch (NoSuchMethodException e) {
146            throw new ConfigException.BadBean(clazz.getName() + " needs a public no-args constructor to be used as a bean", e);
147        } catch (InstantiationException e) {
148            throw new ConfigException.BadBean(clazz.getName() + " needs to be instantiable to be used as a bean", e);
149        } catch (IllegalAccessException e) {
150            throw new ConfigException.BadBean(clazz.getName() + " getters and setters are not accessible, they must be for use as a bean", e);
151        } catch (InvocationTargetException e) {
152            throw new ConfigException.BadBean("Calling bean method on " + clazz.getName() + " caused an exception", e);
153        }
154    }
155
156    // we could magically make this work in many cases by doing
157    // getAnyRef() (or getValue().unwrapped()), but anytime we
158    // rely on that, we aren't doing the type conversions Config
159    // usually does, and we will throw ClassCastException instead
160    // of a nicer error message giving the name of the bad
161    // setting. So, instead, we only support a limited number of
162    // types plus you can always use Object, ConfigValue, Config,
163    // ConfigObject, etc.  as an escape hatch.
164    private static Object getValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config,
165            String configPropName, boolean allowUnknownConfigKeys) {
166        if (parameterClass == Boolean.class || parameterClass == boolean.class) {
167            return config.getBoolean(configPropName);
168        } else if (parameterClass == Integer.class || parameterClass == int.class) {
169            return config.getInt(configPropName);
170        } else if (parameterClass == Double.class || parameterClass == double.class) {
171            return config.getDouble(configPropName);
172        } else if (parameterClass == Long.class || parameterClass == long.class) {
173            return config.getLong(configPropName);
174        } else if (parameterClass == String.class) {
175            return config.getString(configPropName);
176        } else if (parameterClass == Duration.class) {
177            return config.getDuration(configPropName);
178        } else if (parameterClass == ConfigMemorySize.class) {
179            return config.getMemorySize(configPropName);
180        } else if (parameterClass == Object.class) {
181            return config.getAnyRef(configPropName);
182        } else if (parameterClass == List.class) {
183            return getListValue(beanClass, parameterType, parameterClass, config, configPropName,
184                    allowUnknownConfigKeys);
185        } else if (parameterClass == Set.class) {
186            return getSetValue(beanClass, parameterType, parameterClass, config, configPropName,
187                    allowUnknownConfigKeys);
188        } else if (parameterClass == Map.class) {
189            // we could do better here, but right now we don't.
190            Type[] typeArgs = ((ParameterizedType)parameterType).getActualTypeArguments();
191            if (typeArgs[0] != String.class || typeArgs[1] != Object.class) {
192                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");
193            }
194            return config.getObject(configPropName).unwrapped();
195        } else if (parameterClass == Config.class) {
196            return config.getConfig(configPropName);
197        } else if (parameterClass == ConfigObject.class) {
198            return config.getObject(configPropName);
199        } else if (parameterClass == ConfigValue.class) {
200            return config.getValue(configPropName);
201        } else if (parameterClass == ConfigList.class) {
202            return config.getList(configPropName);
203        } else if (parameterClass.isEnum()) {
204            @SuppressWarnings("unchecked")
205            Enum enumValue = config.getEnum((Class<Enum>) parameterClass, configPropName);
206            return enumValue;
207        } else if (hasAtLeastOneBeanProperty(parameterClass)) {
208            return createInternal(config.getConfig(configPropName), parameterClass, allowUnknownConfigKeys);
209        } else {
210            throw new ConfigException.BadBean("Bean property " + configPropName + " of class " + beanClass.getName() + " has unsupported type " + parameterType);
211        }
212    }
213
214    private static Object getSetValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config,
215            String configPropName, boolean allowUnknownConfigKeys) {
216        return new HashSet((List) getListValue(beanClass, parameterType, parameterClass, config, configPropName,
217                allowUnknownConfigKeys));
218    }
219
220    private static Object getListValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config,
221            String configPropName, boolean allowUnknownConfigKeys) {
222        Type elementType = ((ParameterizedType)parameterType).getActualTypeArguments()[0];
223
224        if (elementType == Boolean.class) {
225            return config.getBooleanList(configPropName);
226        } else if (elementType == Integer.class) {
227            return config.getIntList(configPropName);
228        } else if (elementType == Double.class) {
229            return config.getDoubleList(configPropName);
230        } else if (elementType == Long.class) {
231            return config.getLongList(configPropName);
232        } else if (elementType == String.class) {
233            return config.getStringList(configPropName);
234        } else if (elementType == Duration.class) {
235            return config.getDurationList(configPropName);
236        } else if (elementType == ConfigMemorySize.class) {
237            return config.getMemorySizeList(configPropName);
238        } else if (elementType == Object.class) {
239            return config.getAnyRefList(configPropName);
240        } else if (elementType == Config.class) {
241            return config.getConfigList(configPropName);
242        } else if (elementType == ConfigObject.class) {
243            return config.getObjectList(configPropName);
244        } else if (elementType == ConfigValue.class) {
245            return config.getList(configPropName);
246        } else if (((Class<?>) elementType).isEnum()) {
247            @SuppressWarnings("unchecked")
248            List<Enum> enumValues = config.getEnumList((Class<Enum>) elementType, configPropName);
249            return enumValues;
250        } else if (hasAtLeastOneBeanProperty((Class<?>) elementType)) {
251            List<Object> beanList = new ArrayList<Object>();
252            List<? extends Config> configList = config.getConfigList(configPropName);
253            for (Config listMember : configList) {
254                beanList.add(createInternal(listMember, (Class<?>) elementType, allowUnknownConfigKeys));
255            }
256            return beanList;
257        } else {
258            throw new ConfigException.BadBean("Bean property '" + configPropName + "' of class " + beanClass.getName() + " has unsupported list element type " + elementType);
259        }
260    }
261
262    // null if we can't easily say; this is heuristic/best-effort
263    private static ConfigValueType getValueTypeOrNull(Class<?> parameterClass) {
264        if (parameterClass == Boolean.class || parameterClass == boolean.class) {
265            return ConfigValueType.BOOLEAN;
266        } else if (parameterClass == Integer.class || parameterClass == int.class) {
267            return ConfigValueType.NUMBER;
268        } else if (parameterClass == Double.class || parameterClass == double.class) {
269            return ConfigValueType.NUMBER;
270        } else if (parameterClass == Long.class || parameterClass == long.class) {
271            return ConfigValueType.NUMBER;
272        } else if (parameterClass == String.class) {
273            return ConfigValueType.STRING;
274        } else if (parameterClass == Duration.class) {
275            return null;
276        } else if (parameterClass == ConfigMemorySize.class) {
277            return null;
278        } else if (parameterClass == List.class) {
279            return ConfigValueType.LIST;
280        } else if (parameterClass == Map.class) {
281            return ConfigValueType.OBJECT;
282        } else if (parameterClass == Config.class) {
283            return ConfigValueType.OBJECT;
284        } else if (parameterClass == ConfigObject.class) {
285            return ConfigValueType.OBJECT;
286        } else if (parameterClass == ConfigList.class) {
287            return ConfigValueType.LIST;
288        } else {
289            return null;
290        }
291    }
292
293    private static boolean hasAtLeastOneBeanProperty(Class<?> clazz) {
294        BeanInfo beanInfo = null;
295        try {
296            beanInfo = Introspector.getBeanInfo(clazz);
297        } catch (IntrospectionException e) {
298            return false;
299        }
300
301        for (PropertyDescriptor beanProp : beanInfo.getPropertyDescriptors()) {
302            if (beanProp.getReadMethod() != null && beanProp.getWriteMethod() != null) {
303                return true;
304            }
305        }
306
307        return false;
308    }
309
310    private static boolean isOptionalProperty(Class beanClass, PropertyDescriptor beanProp) {
311        Field field = getField(beanClass, beanProp.getName());
312        return field != null ? field.getAnnotationsByType(Optional.class).length > 0 : beanProp.getReadMethod().getAnnotationsByType(Optional.class).length > 0;
313    }
314
315    private static Field getField(Class beanClass, String fieldName) {
316        try {
317            Field field = beanClass.getDeclaredField(fieldName);
318            field.setAccessible(true);
319            return field;
320        } catch (NoSuchFieldException e) {
321            // Don't give up yet. Try to look for field in super class, if any.
322        }
323        beanClass = beanClass.getSuperclass();
324        if (beanClass == null) {
325            return null;
326        }
327        return getField(beanClass, fieldName);
328    }
329}