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.List; 015import java.util.Map; 016import java.time.Duration; 017 018import com.typesafe.config.Config; 019import com.typesafe.config.ConfigObject; 020import com.typesafe.config.ConfigList; 021import com.typesafe.config.ConfigException; 022import com.typesafe.config.ConfigMemorySize; 023import com.typesafe.config.ConfigValue; 024import com.typesafe.config.ConfigValueType; 025import com.typesafe.config.Optional; 026 027/** 028 * Internal implementation detail, not ABI stable, do not touch. 029 * For use only by the {@link com.typesafe.config} package. 030 */ 031public class ConfigBeanImpl { 032 033 /** 034 * This is public ONLY for use by the "config" package, DO NOT USE this ABI 035 * may change. 036 * @param <T> type of the bean 037 * @param config config to use 038 * @param clazz class of the bean 039 * @return the bean instance 040 */ 041 public static <T> T createInternal(Config config, Class<T> clazz) { 042 if (((SimpleConfig)config).root().resolveStatus() != ResolveStatus.RESOLVED) 043 throw new ConfigException.NotResolved( 044 "need to Config#resolve() a config before using it to initialize a bean, see the API docs for Config#resolve()"); 045 046 Map<String, AbstractConfigValue> configProps = new HashMap<String, AbstractConfigValue>(); 047 Map<String, String> originalNames = new HashMap<String, String>(); 048 for (Map.Entry<String, ConfigValue> configProp : config.root().entrySet()) { 049 String originalName = configProp.getKey(); 050 String camelName = ConfigImplUtil.toCamelCase(originalName); 051 // if a setting is in there both as some hyphen name and the camel name, 052 // the camel one wins 053 if (originalNames.containsKey(camelName) && !originalName.equals(camelName)) { 054 // if we aren't a camel name to start with, we lose. 055 // if we are or we are the first matching key, we win. 056 } else { 057 configProps.put(camelName, (AbstractConfigValue) configProp.getValue()); 058 originalNames.put(camelName, originalName); 059 } 060 } 061 062 BeanInfo beanInfo = null; 063 try { 064 beanInfo = Introspector.getBeanInfo(clazz); 065 } catch (IntrospectionException e) { 066 throw new ConfigException.BadBean("Could not get bean information for class " + clazz.getName(), e); 067 } 068 069 try { 070 List<PropertyDescriptor> beanProps = new ArrayList<PropertyDescriptor>(); 071 for (PropertyDescriptor beanProp : beanInfo.getPropertyDescriptors()) { 072 if (beanProp.getReadMethod() == null || beanProp.getWriteMethod() == null) { 073 continue; 074 } 075 beanProps.add(beanProp); 076 } 077 078 // Try to throw all validation issues at once (this does not comprehensively 079 // find every issue, but it should find common ones). 080 List<ConfigException.ValidationProblem> problems = new ArrayList<ConfigException.ValidationProblem>(); 081 for (PropertyDescriptor beanProp : beanProps) { 082 Method setter = beanProp.getWriteMethod(); 083 Class<?> parameterClass = setter.getParameterTypes()[0]; 084 085 ConfigValueType expectedType = getValueTypeOrNull(parameterClass); 086 if (expectedType != null) { 087 String name = originalNames.get(beanProp.getName()); 088 if (name == null) 089 name = beanProp.getName(); 090 Path path = Path.newKey(name); 091 AbstractConfigValue configValue = configProps.get(beanProp.getName()); 092 if (configValue != null) { 093 SimpleConfig.checkValid(path, expectedType, configValue, problems); 094 } else { 095 if (!isOptionalProperty(clazz, beanProp)) { 096 SimpleConfig.addMissing(problems, expectedType, path, config.origin()); 097 } 098 } 099 } 100 } 101 102 if (!problems.isEmpty()) { 103 throw new ConfigException.ValidationFailed(problems); 104 } 105 106 // Fill in the bean instance 107 T bean = clazz.newInstance(); 108 for (PropertyDescriptor beanProp : beanProps) { 109 Method setter = beanProp.getWriteMethod(); 110 Type parameterType = setter.getGenericParameterTypes()[0]; 111 Class<?> parameterClass = setter.getParameterTypes()[0]; 112 String configPropName = originalNames.get(beanProp.getName()); 113 // Is the property key missing in the config? 114 if (configPropName == null) { 115 // If so, continue if the field is marked as @{link Optional} 116 if (isOptionalProperty(clazz, beanProp)) { 117 continue; 118 } 119 // Otherwise, raise a {@link Missing} exception right here 120 throw new ConfigException.Missing(beanProp.getName()); 121 } 122 Object unwrapped = getValue(clazz, parameterType, parameterClass, config, configPropName); 123 setter.invoke(bean, unwrapped); 124 } 125 return bean; 126 } catch (InstantiationException e) { 127 throw new ConfigException.BadBean(clazz.getName() + " needs a public no-args constructor to be used as a bean", e); 128 } catch (IllegalAccessException e) { 129 throw new ConfigException.BadBean(clazz.getName() + " getters and setters are not accessible, they must be for use as a bean", e); 130 } catch (InvocationTargetException e) { 131 throw new ConfigException.BadBean("Calling bean method on " + clazz.getName() + " caused an exception", e); 132 } 133 } 134 135 // we could magically make this work in many cases by doing 136 // getAnyRef() (or getValue().unwrapped()), but anytime we 137 // rely on that, we aren't doing the type conversions Config 138 // usually does, and we will throw ClassCastException instead 139 // of a nicer error message giving the name of the bad 140 // setting. So, instead, we only support a limited number of 141 // types plus you can always use Object, ConfigValue, Config, 142 // ConfigObject, etc. as an escape hatch. 143 private static Object getValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, 144 String configPropName) { 145 if (parameterClass == Boolean.class || parameterClass == boolean.class) { 146 return config.getBoolean(configPropName); 147 } else if (parameterClass == Integer.class || parameterClass == int.class) { 148 return config.getInt(configPropName); 149 } else if (parameterClass == Double.class || parameterClass == double.class) { 150 return config.getDouble(configPropName); 151 } else if (parameterClass == Long.class || parameterClass == long.class) { 152 return config.getLong(configPropName); 153 } else if (parameterClass == String.class) { 154 return config.getString(configPropName); 155 } else if (parameterClass == Duration.class) { 156 return config.getDuration(configPropName); 157 } else if (parameterClass == ConfigMemorySize.class) { 158 return config.getMemorySize(configPropName); 159 } else if (parameterClass == Object.class) { 160 return config.getAnyRef(configPropName); 161 } else if (parameterClass == List.class) { 162 return getListValue(beanClass, parameterType, parameterClass, config, configPropName); 163 } else if (parameterClass == Map.class) { 164 // we could do better here, but right now we don't. 165 Type[] typeArgs = ((ParameterizedType)parameterType).getActualTypeArguments(); 166 if (typeArgs[0] != String.class || typeArgs[1] != Object.class) { 167 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"); 168 } 169 return config.getObject(configPropName).unwrapped(); 170 } else if (parameterClass == Config.class) { 171 return config.getConfig(configPropName); 172 } else if (parameterClass == ConfigObject.class) { 173 return config.getObject(configPropName); 174 } else if (parameterClass == ConfigValue.class) { 175 return config.getValue(configPropName); 176 } else if (parameterClass == ConfigList.class) { 177 return config.getList(configPropName); 178 } else if (parameterClass.isEnum()) { 179 @SuppressWarnings("unchecked") 180 Enum enumValue = config.getEnum((Class<Enum>) parameterClass, configPropName); 181 return enumValue; 182 } else if (hasAtLeastOneBeanProperty(parameterClass)) { 183 return createInternal(config.getConfig(configPropName), parameterClass); 184 } else { 185 throw new ConfigException.BadBean("Bean property " + configPropName + " of class " + beanClass.getName() + " has unsupported type " + parameterType); 186 } 187 } 188 189 private static Object getListValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPropName) { 190 Type elementType = ((ParameterizedType)parameterType).getActualTypeArguments()[0]; 191 192 if (elementType == Boolean.class) { 193 return config.getBooleanList(configPropName); 194 } else if (elementType == Integer.class) { 195 return config.getIntList(configPropName); 196 } else if (elementType == Double.class) { 197 return config.getDoubleList(configPropName); 198 } else if (elementType == Long.class) { 199 return config.getLongList(configPropName); 200 } else if (elementType == String.class) { 201 return config.getStringList(configPropName); 202 } else if (elementType == Duration.class) { 203 return config.getDurationList(configPropName); 204 } else if (elementType == ConfigMemorySize.class) { 205 return config.getMemorySizeList(configPropName); 206 } else if (elementType == Object.class) { 207 return config.getAnyRefList(configPropName); 208 } else if (elementType == Config.class) { 209 return config.getConfigList(configPropName); 210 } else if (elementType == ConfigObject.class) { 211 return config.getObjectList(configPropName); 212 } else if (elementType == ConfigValue.class) { 213 return config.getList(configPropName); 214 } else if (((Class<?>) elementType).isEnum()) { 215 @SuppressWarnings("unchecked") 216 List<Enum> enumValues = config.getEnumList((Class<Enum>) elementType, configPropName); 217 return enumValues; 218 } else if (hasAtLeastOneBeanProperty((Class<?>) elementType)) { 219 List<Object> beanList = new ArrayList<Object>(); 220 List<? extends Config> configList = config.getConfigList(configPropName); 221 for (Config listMember : configList) { 222 beanList.add(createInternal(listMember, (Class<?>) elementType)); 223 } 224 return beanList; 225 } else { 226 throw new ConfigException.BadBean("Bean property '" + configPropName + "' of class " + beanClass.getName() + " has unsupported list element type " + elementType); 227 } 228 } 229 230 // null if we can't easily say; this is heuristic/best-effort 231 private static ConfigValueType getValueTypeOrNull(Class<?> parameterClass) { 232 if (parameterClass == Boolean.class || parameterClass == boolean.class) { 233 return ConfigValueType.BOOLEAN; 234 } else if (parameterClass == Integer.class || parameterClass == int.class) { 235 return ConfigValueType.NUMBER; 236 } else if (parameterClass == Double.class || parameterClass == double.class) { 237 return ConfigValueType.NUMBER; 238 } else if (parameterClass == Long.class || parameterClass == long.class) { 239 return ConfigValueType.NUMBER; 240 } else if (parameterClass == String.class) { 241 return ConfigValueType.STRING; 242 } else if (parameterClass == Duration.class) { 243 return null; 244 } else if (parameterClass == ConfigMemorySize.class) { 245 return null; 246 } else if (parameterClass == List.class) { 247 return ConfigValueType.LIST; 248 } else if (parameterClass == Map.class) { 249 return ConfigValueType.OBJECT; 250 } else if (parameterClass == Config.class) { 251 return ConfigValueType.OBJECT; 252 } else if (parameterClass == ConfigObject.class) { 253 return ConfigValueType.OBJECT; 254 } else if (parameterClass == ConfigList.class) { 255 return ConfigValueType.LIST; 256 } else { 257 return null; 258 } 259 } 260 261 private static boolean hasAtLeastOneBeanProperty(Class<?> clazz) { 262 BeanInfo beanInfo = null; 263 try { 264 beanInfo = Introspector.getBeanInfo(clazz); 265 } catch (IntrospectionException e) { 266 return false; 267 } 268 269 for (PropertyDescriptor beanProp : beanInfo.getPropertyDescriptors()) { 270 if (beanProp.getReadMethod() != null && beanProp.getWriteMethod() != null) { 271 return true; 272 } 273 } 274 275 return false; 276 } 277 278 private static boolean isOptionalProperty(Class beanClass, PropertyDescriptor beanProp) { 279 Field field = getField(beanClass, beanProp.getName()); 280 return (field.getAnnotationsByType(Optional.class).length > 0); 281 } 282 283 private static Field getField(Class beanClass, String fieldName) { 284 try { 285 Field field = beanClass.getDeclaredField(fieldName); 286 field.setAccessible(true); 287 return field; 288 } catch (NoSuchFieldException e) { 289 // Don't give up yet. Try to look for field in super class, if any. 290 } 291 beanClass = beanClass.getSuperclass(); 292 if (beanClass == null) { 293 return null; 294 } 295 return getField(beanClass, fieldName); 296 } 297}