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}