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}