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