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.util.concurrent.TimeUnit; 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; 025 026/** 027 * Internal implementation detail, not ABI stable, do not touch. 028 * For use only by the {@link com.typesafe.config} package. 029 */ 030public class ConfigBeanImpl { 031 032 /** 033 * This is public ONLY for use by the "config" package, DO NOT USE this ABI 034 * may change. 035 * @param <T> type of the bean 036 * @param config config to use 037 * @param clazz class of the bean 038 * @return the bean instance 039 */ 040 public static <T> T createInternal(Config config, Class<T> clazz) { 041 if (((SimpleConfig)config).root().resolveStatus() != ResolveStatus.RESOLVED) 042 throw new ConfigException.NotResolved( 043 "need to Config#resolve() a config before using it to initialize a bean, see the API docs for Config#resolve()"); 044 045 Map<String, AbstractConfigValue> configProps = new HashMap<String, AbstractConfigValue>(); 046 Map<String, String> originalNames = new HashMap<String, String>(); 047 for (Map.Entry<String, ConfigValue> configProp : config.root().entrySet()) { 048 String originalName = configProp.getKey(); 049 String camelName = ConfigImplUtil.toCamelCase(originalName); 050 // if a setting is in there both as some hyphen name and the camel name, 051 // the camel one wins 052 if (originalNames.containsKey(camelName) && originalName != camelName) { 053 // if we aren't a camel name to start with, we lose. 054 // if we are or we are the first matching key, we win. 055 } else { 056 configProps.put(camelName, (AbstractConfigValue) configProp.getValue()); 057 originalNames.put(camelName, originalName); 058 } 059 } 060 061 BeanInfo beanInfo = null; 062 try { 063 beanInfo = Introspector.getBeanInfo(clazz); 064 } catch (IntrospectionException e) { 065 throw new ConfigException.BadBean("Could not get bean information for class " + clazz.getName(), e); 066 } 067 068 try { 069 List<PropertyDescriptor> beanProps = new ArrayList<PropertyDescriptor>(); 070 for (PropertyDescriptor beanProp : beanInfo.getPropertyDescriptors()) { 071 if (beanProp.getReadMethod() == null || beanProp.getWriteMethod() == null) { 072 continue; 073 } 074 beanProps.add(beanProp); 075 } 076 077 // Try to throw all validation issues at once (this does not comprehensively 078 // find every issue, but it should find common ones). 079 List<ConfigException.ValidationProblem> problems = new ArrayList<ConfigException.ValidationProblem>(); 080 for (PropertyDescriptor beanProp : beanProps) { 081 Method setter = beanProp.getWriteMethod(); 082 Class parameterClass = setter.getParameterTypes()[0]; 083 084 ConfigValueType expectedType = getValueTypeOrNull(parameterClass); 085 if (expectedType != null) { 086 String name = originalNames.get(beanProp.getName()); 087 if (name == null) 088 name = beanProp.getName(); 089 Path path = Path.newKey(name); 090 AbstractConfigValue configValue = configProps.get(beanProp.getName()); 091 if (configValue != null) { 092 SimpleConfig.checkValid(path, expectedType, configValue, problems); 093 } else { 094 SimpleConfig.addMissing(problems, expectedType, path, config.origin()); 095 } 096 } 097 } 098 099 if (!problems.isEmpty()) { 100 throw new ConfigException.ValidationFailed(problems); 101 } 102 103 // Fill in the bean instance 104 T bean = clazz.newInstance(); 105 for (PropertyDescriptor beanProp : beanProps) { 106 Method setter = beanProp.getWriteMethod(); 107 Type parameterType = setter.getGenericParameterTypes()[0]; 108 Class parameterClass = setter.getParameterTypes()[0]; 109 Object unwrapped = getValue(clazz, parameterType, parameterClass, config, originalNames.get(beanProp.getName())); 110 setter.invoke(bean, unwrapped); 111 } 112 return bean; 113 } catch (InstantiationException e) { 114 throw new ConfigException.BadBean(clazz.getName() + " needs a public no-args constructor to be used as a bean", e); 115 } catch (IllegalAccessException e) { 116 throw new ConfigException.BadBean(clazz.getName() + " getters and setters are not accessible, they must be for use as a bean", e); 117 } catch (InvocationTargetException e) { 118 throw new ConfigException.BadBean("Calling bean method on " + clazz.getName() + " caused an exception", e); 119 } 120 } 121 122 // we could magically make this work in many cases by doing 123 // getAnyRef() (or getValue().unwrapped()), but anytime we 124 // rely on that, we aren't doing the type conversions Config 125 // usually does, and we will throw ClassCastException instead 126 // of a nicer error message giving the name of the bad 127 // setting. So, instead, we only support a limited number of 128 // types plus you can always use Object, ConfigValue, Config, 129 // ConfigObject, etc. as an escape hatch. 130 private static Object getValue(Class beanClass, Type parameterType, Class parameterClass, Config config, 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}