package com.drtshock.playervaults.lib.com.typesafe.config.impl; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.time.Duration; import java.util.Set; import com.drtshock.playervaults.lib.com.typesafe.config.Config; import com.drtshock.playervaults.lib.com.typesafe.config.ConfigObject; import com.drtshock.playervaults.lib.com.typesafe.config.ConfigList; import com.drtshock.playervaults.lib.com.typesafe.config.ConfigException; import com.drtshock.playervaults.lib.com.typesafe.config.ConfigMemorySize; import com.drtshock.playervaults.lib.com.typesafe.config.ConfigValue; import com.drtshock.playervaults.lib.com.typesafe.config.ConfigValueType; import com.drtshock.playervaults.lib.com.typesafe.config.Optional; /** * Internal implementation detail, not ABI stable, do not touch. * For use only by the {@link com.typesafe.config} package. */ public class ConfigBeanImpl { /** * This is public ONLY for use by the "config" package, DO NOT USE this ABI * may change. * @param <T> type of the bean * @param config config to use * @param clazz class of the bean * @return the bean instance */ public static <T> T createInternal(Config config, Class<T> clazz) { if (((SimpleConfig)config).root().resolveStatus() != ResolveStatus.RESOLVED) throw new ConfigException.NotResolved( "need to Config#resolve() a config before using it to initialize a bean, see the API docs for Config#resolve()"); Map<String, AbstractConfigValue> configProps = new HashMap<String, AbstractConfigValue>(); Map<String, String> originalNames = new HashMap<String, String>(); for (Map.Entry<String, ConfigValue> configProp : config.root().entrySet()) { String originalName = configProp.getKey(); String camelName = ConfigImplUtil.toCamelCase(originalName); // if a setting is in there both as some hyphen name and the camel name, // the camel one wins if (originalNames.containsKey(camelName) && !originalName.equals(camelName)) { // if we aren't a camel name to start with, we lose. // if we are or we are the first matching key, we win. } else { configProps.put(camelName, (AbstractConfigValue) configProp.getValue()); originalNames.put(camelName, originalName); } } BeanInfo beanInfo = null; try { beanInfo = Introspector.getBeanInfo(clazz); } catch (IntrospectionException e) { throw new ConfigException.BadBean("Could not get bean information for class " + clazz.getName(), e); } try { List<PropertyDescriptor> beanProps = new ArrayList<PropertyDescriptor>(); for (PropertyDescriptor beanProp : beanInfo.getPropertyDescriptors()) { if (beanProp.getReadMethod() == null || beanProp.getWriteMethod() == null) { continue; } beanProps.add(beanProp); } // Try to throw all validation issues at once (this does not comprehensively // find every issue, but it should find common ones). List<ConfigException.ValidationProblem> problems = new ArrayList<ConfigException.ValidationProblem>(); for (PropertyDescriptor beanProp : beanProps) { Method setter = beanProp.getWriteMethod(); Class<?> parameterClass = setter.getParameterTypes()[0]; ConfigValueType expectedType = getValueTypeOrNull(parameterClass); if (expectedType != null) { String name = originalNames.get(beanProp.getName()); if (name == null) name = beanProp.getName(); Path path = Path.newKey(name); AbstractConfigValue configValue = configProps.get(beanProp.getName()); if (configValue != null) { SimpleConfig.checkValid(path, expectedType, configValue, problems); } else { if (!isOptionalProperty(clazz, beanProp)) { SimpleConfig.addMissing(problems, expectedType, path, config.origin()); } } } } if (!problems.isEmpty()) { throw new ConfigException.ValidationFailed(problems); } // Fill in the bean instance T bean = clazz.newInstance(); for (PropertyDescriptor beanProp : beanProps) { Method setter = beanProp.getWriteMethod(); Type parameterType = setter.getGenericParameterTypes()[0]; Class<?> parameterClass = setter.getParameterTypes()[0]; String configPropName = originalNames.get(beanProp.getName()); // Is the property key missing in the config? if (configPropName == null) { // If so, continue if the field is marked as @{link Optional} if (isOptionalProperty(clazz, beanProp)) { continue; } // Otherwise, raise a {@link Missing} exception right here throw new ConfigException.Missing(beanProp.getName()); } Object unwrapped = getValue(clazz, parameterType, parameterClass, config, configPropName); setter.invoke(bean, unwrapped); } return bean; } catch (InstantiationException e) { throw new ConfigException.BadBean(clazz.getName() + " needs a public no-args constructor to be used as a bean", e); } catch (IllegalAccessException e) { throw new ConfigException.BadBean(clazz.getName() + " getters and setters are not accessible, they must be for use as a bean", e); } catch (InvocationTargetException e) { throw new ConfigException.BadBean("Calling bean method on " + clazz.getName() + " caused an exception", e); } } // we could magically make this work in many cases by doing // getAnyRef() (or getValue().unwrapped()), but anytime we // rely on that, we aren't doing the type conversions Config // usually does, and we will throw ClassCastException instead // of a nicer error message giving the name of the bad // setting. So, instead, we only support a limited number of // types plus you can always use Object, ConfigValue, Config, // ConfigObject, etc. as an escape hatch. private static Object getValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPropName) { if (parameterClass == Boolean.class || parameterClass == boolean.class) { return config.getBoolean(configPropName); } else if (parameterClass == Integer.class || parameterClass == int.class) { return config.getInt(configPropName); } else if (parameterClass == Double.class || parameterClass == double.class) { return config.getDouble(configPropName); } else if (parameterClass == Long.class || parameterClass == long.class) { return config.getLong(configPropName); } else if (parameterClass == String.class) { return config.getString(configPropName); } else if (parameterClass == Duration.class) { return config.getDuration(configPropName); } else if (parameterClass == ConfigMemorySize.class) { return config.getMemorySize(configPropName); } else if (parameterClass == Object.class) { return config.getAnyRef(configPropName); } else if (parameterClass == List.class) { return getListValue(beanClass, parameterType, parameterClass, config, configPropName); } else if (parameterClass == Set.class) { return getSetValue(beanClass, parameterType, parameterClass, config, configPropName); } else if (parameterClass == Map.class) { // we could do better here, but right now we don't. Type[] typeArgs = ((ParameterizedType)parameterType).getActualTypeArguments(); if (typeArgs[0] != String.class || typeArgs[1] != Object.class) { 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"); } return config.getObject(configPropName).unwrapped(); } else if (parameterClass == Config.class) { return config.getConfig(configPropName); } else if (parameterClass == ConfigObject.class) { return config.getObject(configPropName); } else if (parameterClass == ConfigValue.class) { return config.getValue(configPropName); } else if (parameterClass == ConfigList.class) { return config.getList(configPropName); } else if (parameterClass.isEnum()) { @SuppressWarnings("unchecked") Enum enumValue = config.getEnum((Class<Enum>) parameterClass, configPropName); return enumValue; } else if (hasAtLeastOneBeanProperty(parameterClass)) { return createInternal(config.getConfig(configPropName), parameterClass); } else { throw new ConfigException.BadBean("Bean property " + configPropName + " of class " + beanClass.getName() + " has unsupported type " + parameterType); } } private static Object getSetValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPropName) { return new HashSet((List) getListValue(beanClass, parameterType, parameterClass, config, configPropName)); } private static Object getListValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPropName) { Type elementType = ((ParameterizedType)parameterType).getActualTypeArguments()[0]; if (elementType == Boolean.class) { return config.getBooleanList(configPropName); } else if (elementType == Integer.class) { return config.getIntList(configPropName); } else if (elementType == Double.class) { return config.getDoubleList(configPropName); } else if (elementType == Long.class) { return config.getLongList(configPropName); } else if (elementType == String.class) { return config.getStringList(configPropName); } else if (elementType == Duration.class) { return config.getDurationList(configPropName); } else if (elementType == ConfigMemorySize.class) { return config.getMemorySizeList(configPropName); } else if (elementType == Object.class) { return config.getAnyRefList(configPropName); } else if (elementType == Config.class) { return config.getConfigList(configPropName); } else if (elementType == ConfigObject.class) { return config.getObjectList(configPropName); } else if (elementType == ConfigValue.class) { return config.getList(configPropName); } else if (((Class<?>) elementType).isEnum()) { @SuppressWarnings("unchecked") List<Enum> enumValues = config.getEnumList((Class<Enum>) elementType, configPropName); return enumValues; } else if (hasAtLeastOneBeanProperty((Class<?>) elementType)) { List<Object> beanList = new ArrayList<Object>(); List<? extends Config> configList = config.getConfigList(configPropName); for (Config listMember : configList) { beanList.add(createInternal(listMember, (Class<?>) elementType)); } return beanList; } else { throw new ConfigException.BadBean("Bean property '" + configPropName + "' of class " + beanClass.getName() + " has unsupported list element type " + elementType); } } // null if we can't easily say; this is heuristic/best-effort private static ConfigValueType getValueTypeOrNull(Class<?> parameterClass) { if (parameterClass == Boolean.class || parameterClass == boolean.class) { return ConfigValueType.BOOLEAN; } else if (parameterClass == Integer.class || parameterClass == int.class) { return ConfigValueType.NUMBER; } else if (parameterClass == Double.class || parameterClass == double.class) { return ConfigValueType.NUMBER; } else if (parameterClass == Long.class || parameterClass == long.class) { return ConfigValueType.NUMBER; } else if (parameterClass == String.class) { return ConfigValueType.STRING; } else if (parameterClass == Duration.class) { return null; } else if (parameterClass == ConfigMemorySize.class) { return null; } else if (parameterClass == List.class) { return ConfigValueType.LIST; } else if (parameterClass == Map.class) { return ConfigValueType.OBJECT; } else if (parameterClass == Config.class) { return ConfigValueType.OBJECT; } else if (parameterClass == ConfigObject.class) { return ConfigValueType.OBJECT; } else if (parameterClass == ConfigList.class) { return ConfigValueType.LIST; } else { return null; } } private static boolean hasAtLeastOneBeanProperty(Class<?> clazz) { BeanInfo beanInfo = null; try { beanInfo = Introspector.getBeanInfo(clazz); } catch (IntrospectionException e) { return false; } for (PropertyDescriptor beanProp : beanInfo.getPropertyDescriptors()) { if (beanProp.getReadMethod() != null && beanProp.getWriteMethod() != null) { return true; } } return false; } private static boolean isOptionalProperty(Class beanClass, PropertyDescriptor beanProp) { Field field = getField(beanClass, beanProp.getName()); return field != null && (field.getAnnotationsByType(Optional.class).length > 0); } private static Field getField(Class beanClass, String fieldName) { try { Field field = beanClass.getDeclaredField(fieldName); field.setAccessible(true); return field; } catch (NoSuchFieldException e) { // Don't give up yet. Try to look for field in super class, if any. } beanClass = beanClass.getSuperclass(); if (beanClass == null) { return null; } return getField(beanClass, fieldName); } }