1
2
3
4
5
6
7
8
9
10
11
12
13
14 package com.addthis.codec.config;
15
16 import javax.annotation.Nonnull;
17 import javax.annotation.Syntax;
18
19 import java.io.IOException;
20
21 import java.lang.reflect.Modifier;
22
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.concurrent.atomic.AtomicBoolean;
29
30 import com.addthis.codec.jackson.Jackson;
31 import com.addthis.codec.plugins.PluginMap;
32 import com.addthis.codec.plugins.PluginRegistry;
33 import com.addthis.codec.reflection.CodableClassInfo;
34 import com.addthis.codec.reflection.CodableFieldInfo;
35
36 import com.google.common.annotations.Beta;
37 import com.google.common.base.Objects;
38
39 import com.typesafe.config.Config;
40 import com.typesafe.config.ConfigException;
41 import com.typesafe.config.ConfigFactory;
42 import com.typesafe.config.ConfigList;
43 import com.typesafe.config.ConfigObject;
44 import com.typesafe.config.ConfigValue;
45 import com.typesafe.config.ConfigValueFactory;
46 import com.typesafe.config.ConfigValueType;
47
48 @Beta
49 public final class Configs {
50 private Configs() {}
51
52
53
54
55
56 public static <T> T newDefault(@Nonnull Class<T> type) throws IOException {
57 return Jackson.defaultCodec().newDefault(type);
58 }
59
60
61 public static <T> T newDefault(@Nonnull String category) throws IOException {
62 return Jackson.defaultCodec().newDefault(category);
63 }
64
65
66
67
68
69
70 public static <T> T decodeObject(@Nonnull Class<T> type, @Nonnull Config config) throws IOException {
71 return Jackson.defaultCodec().decodeObject(type, config);
72 }
73
74
75
76
77
78
79 public static <T> T decodeObject(@Nonnull String category, @Nonnull Config config) throws IOException {
80 return Jackson.defaultCodec().decodeObject(category, config);
81 }
82
83
84
85
86
87
88 public static <T> T decodeObject(@Nonnull Class<T> type, @Syntax("HOCON") @Nonnull String configText)
89 throws IOException {
90 return Jackson.defaultCodec().decodeObject(type, configText);
91 }
92
93
94
95
96
97
98 public static <T> T decodeObject(@Nonnull String category, @Syntax("HOCON") @Nonnull String configText)
99 throws IOException {
100 return Jackson.defaultCodec().decodeObject(category, configText);
101 }
102
103
104
105
106
107
108 public static <T> T decodeObject(@Syntax("HOCON") String configText) throws IOException {
109 return Jackson.defaultCodec().decodeObject(configText);
110 }
111
112
113
114
115
116
117 public static <T> T decodeObject(Config config) throws IOException {
118 return Jackson.defaultCodec().decodeObject(config);
119 }
120
121 public static ConfigValue expandSugar(Config config, PluginRegistry pluginRegistry) {
122 if (config.root().size() != 1) {
123 throw new ConfigException.Parse(config.root().origin(),
124 "config root must have exactly one key");
125 }
126 String category = config.root().keySet().iterator().next();
127 PluginMap pluginMap = pluginRegistry.asMap().get(category);
128 if (pluginMap == null) {
129 throw new ConfigException.BadValue(config.root().get(category).origin(),
130 category,
131 "top level key must be a valid category");
132 }
133 Class<?> baseClass = Objects.firstNonNull(pluginMap.baseClass(), Object.class);
134 return expandSugar(baseClass, config.root().get(category), pluginRegistry);
135 }
136
137 public static ConfigValue expandSugar(Class<?> type, ConfigValue config,
138 PluginRegistry pluginRegistry) {
139 if ((type != null) && type.isAssignableFrom(ConfigValue.class)) {
140 return ConfigValueFactory.fromAnyRef(config.unwrapped(),
141 "unchanged for raw ConfigValue field "
142 + config.origin().description());
143 }
144 CodableClassInfo typeInfo = new CodableClassInfo(type, pluginRegistry.config(), pluginRegistry);
145 PluginMap pluginMap = typeInfo.getPluginMap();
146 ConfigValue valueOrResolvedRoot = resolveType(type, config, pluginMap);
147 if (valueOrResolvedRoot.valueType() != ConfigValueType.OBJECT) {
148 return valueOrResolvedRoot;
149 }
150 ConfigObject root = (ConfigObject) valueOrResolvedRoot;
151 String classField = pluginMap.classField();
152 if (root.get(classField) != null) {
153 try {
154 type = pluginMap.getClass((String) root.get(classField).unwrapped());
155 } catch (ClassNotFoundException ignored) {
156
157 }
158 }
159 return expandSugarSkipResolve(type, root, pluginRegistry);
160 }
161
162 private static ConfigObject expandSugarSkipResolve(Class<?> type, ConfigObject root,
163 PluginRegistry pluginRegistry) {
164 CodableClassInfo resolvedTypeInfo = new CodableClassInfo(type, pluginRegistry.config(), pluginRegistry);
165 ConfigObject fieldDefaults = resolvedTypeInfo.getFieldDefaults().root();
166 for (CodableFieldInfo fieldInfo : resolvedTypeInfo.values()) {
167 String fieldName = fieldInfo.getName();
168 ConfigValue fieldValue = root.get(fieldName);
169 if ((fieldValue == null) && (fieldDefaults.get(fieldName) != null)) {
170 ConfigValue fieldDefault = fieldDefaults.get(fieldName);
171 fieldValue = ConfigValueFactory.fromAnyRef(
172 fieldDefault.unwrapped(), "global default : " + fieldDefault.origin().description());
173 root = root.withValue(fieldName, fieldValue);
174 }
175 if (fieldValue == null) {
176 continue;
177 }
178 if ((fieldInfo.isArray() || fieldInfo.isCollection()) &&
179 (fieldValue.valueType() != ConfigValueType.LIST)) {
180 fieldValue = ConfigValueFactory.fromIterable(
181 Collections.singletonList(fieldValue.unwrapped()), "auto collection of " +
182 fieldValue.origin().description());
183 root = root.withValue(fieldName, fieldValue);
184 }
185 if (!isCodableType(fieldInfo)) {
186 continue;
187 }
188 if (fieldInfo.isArray() || fieldInfo.isCollection()) {
189 if (fieldValue.valueType() != ConfigValueType.LIST) {
190 throw new ConfigException.WrongType(fieldValue.origin(), fieldName,
191 ConfigValueType.LIST.name(), fieldValue.valueType().name());
192 }
193 Class<?> elementType = elementType(fieldInfo);
194 boolean nested = fieldInfo.isCollectionArray();
195 fieldValue = expandSugarArray(fieldValue, elementType, pluginRegistry, nested);
196 } else if (fieldInfo.isMap()) {
197 if (fieldValue.valueType() != ConfigValueType.OBJECT) {
198 throw new ConfigException.WrongType(fieldValue.origin(), fieldName,
199 ConfigValueType.OBJECT.name(), fieldValue.valueType().name());
200 }
201 Class<?> elementType = elementType(fieldInfo);
202 boolean nested = fieldInfo.isMapValueArray();
203 ConfigObject fieldMap = (ConfigObject) fieldValue;
204 Map<String, Object> newMap = new HashMap<>(fieldMap.size());
205 for (Map.Entry<String, ConfigValue> mapEntry : fieldMap.entrySet()) {
206 ConfigValue mapValue = mapEntry.getValue();
207 String mapKey = mapEntry.getKey();
208 ConfigValue resolvedMapObject;
209 if (nested) {
210 resolvedMapObject = expandSugarArray(mapValue, elementType, pluginRegistry, false);
211 } else {
212 resolvedMapObject = expandSugar(elementType, mapValue, pluginRegistry);
213 }
214 newMap.put(mapKey, resolvedMapObject.unwrapped());
215 }
216 fieldValue = ConfigValueFactory.fromMap(newMap, fieldMap.origin().description());
217 } else {
218 fieldValue = expandSugar(fieldInfo.getTypeOrComponentType(), fieldValue, pluginRegistry);
219 }
220 root = root.withValue(fieldName, fieldValue);
221 }
222 return root;
223 }
224
225 private static ConfigList expandSugarArray(ConfigValue fieldValue,
226 Class<?> elementType,
227 PluginRegistry pluginRegistry,
228 boolean nested) {
229 ConfigList fieldList = (ConfigList) fieldValue;
230 List<Object> newList = new ArrayList<>(fieldList.size());
231 for (ConfigValue listEntry : fieldList) {
232 ConfigValue listObject;
233 if (nested) {
234 listObject = expandSugarArray(listEntry, elementType, pluginRegistry, false);
235 } else {
236 listObject = expandSugar(elementType, listEntry, pluginRegistry);
237 }
238 newList.add(listObject.unwrapped());
239 }
240 return ConfigValueFactory.fromIterable(newList, fieldList.origin().description());
241 }
242
243 private static Class<?> elementType(CodableFieldInfo fieldInfo) {
244 Class<?> elementType = fieldInfo.getTypeOrComponentType();
245 if (fieldInfo.isMap()) {
246 elementType = fieldInfo.getMapValueClass();
247 } else if (fieldInfo.isCollection()) {
248 elementType = fieldInfo.getCollectionClass();
249 }
250 return elementType;
251 }
252
253 private static boolean isCodableType(CodableFieldInfo fieldInfo) {
254 Class<?> expectedType = elementType(fieldInfo);
255 if (expectedType.isAssignableFrom(String.class)) {
256 return false;
257 } else if ((expectedType == boolean.class) || (expectedType == Boolean.class)) {
258 return false;
259 } else if (expectedType == AtomicBoolean.class) {
260 return false;
261 } else if (Number.class.isAssignableFrom(expectedType) || expectedType.isPrimitive()) {
262
263 return false;
264 } else if (expectedType.isEnum()) {
265 return false;
266 } else {
267 return true;
268 }
269 }
270
271
272 private static ConfigValue resolveType(Class<?> type, ConfigValue configValue, PluginMap pluginMap) {
273 String classField = pluginMap.classField();
274 if (configValue.valueType() != ConfigValueType.OBJECT) {
275 if ((type == null)
276 || Modifier.isAbstract(type.getModifiers()) || Modifier.isInterface(type.getModifiers())) {
277 if (configValue.valueType() == ConfigValueType.LIST) {
278 Class<?> arrayType = pluginMap.arraySugar();
279 if (arrayType != null) {
280 ConfigObject aliasDefaults = pluginMap.aliasDefaults("_array");
281 String arrayFieldName = aliasDefaults.toConfig().getString("_primary");
282 String arraySugarName = pluginMap.getLastAlias("_array");
283 return ConfigFactory.empty().root().withValue(
284 classField, ConfigValueFactory.fromAnyRef(
285 arraySugarName, pluginMap.category() + " array sugar : " +
286 pluginMap.config().root().get("_array").origin().description()))
287 .withValue(arrayFieldName, configValue)
288 .withFallback(aliasDefaults);
289 }
290 }
291 }
292 return configValue;
293 }
294 ConfigObject root = (ConfigObject) configValue;
295 ConfigValue classValue = root.get(classField);
296
297 if ((classValue != null) && (classValue.valueType() == ConfigValueType.STRING)) {
298 String classValueString = (String) classValue.unwrapped();
299 ConfigObject aliasDefaults = pluginMap.aliasDefaults(classValueString);
300 return root.withFallback(aliasDefaults);
301 }
302
303 if ((type == null) || Modifier.isAbstract(type.getModifiers()) || Modifier.isInterface(type.getModifiers())) {
304
305 if (root.size() == 1) {
306 String onlyKey = root.keySet().iterator().next();
307 try {
308 pluginMap.getClass(onlyKey);
309 ConfigValue onlyKeyValue = root.values().iterator().next();
310 ConfigObject aliasDefaults = pluginMap.aliasDefaults(onlyKey);
311 if (onlyKeyValue.valueType() != ConfigValueType.OBJECT) {
312 if (aliasDefaults.get("_primary") != null) {
313 onlyKeyValue = onlyKeyValue.atPath((String) aliasDefaults.get("_primary").unwrapped()).root();
314 } else {
315 throw new ConfigException.WrongType(onlyKeyValue.origin(), onlyKey,
316 "OBJECT", onlyKeyValue.valueType().toString());
317 }
318 }
319 ConfigObject fieldValues = (ConfigObject) onlyKeyValue;
320 return fieldValues.withValue(classField, ConfigValueFactory.fromAnyRef(
321 onlyKey, "single key to type from " + root.origin().description()))
322 .withFallback(aliasDefaults);
323 } catch (ClassNotFoundException ignored) {
324 }
325 }
326
327
328 String matched = null;
329 for (String alias : pluginMap.inlinedAliases()) {
330 if (root.get(alias) != null) {
331 if (matched != null) {
332 String message = String.format(
333 "no type specified, more than one key, and both %s and %s match for inlined types.",
334 matched, alias);
335 throw new ConfigException.Parse(root.origin(), message);
336 }
337 matched = alias;
338 }
339 }
340 if (matched != null) {
341 ConfigObject aliasDefaults = pluginMap.aliasDefaults(matched);
342 ConfigValue inlinedValue = root.get(matched);
343 String primaryField = (String) aliasDefaults.get("_primary").unwrapped();
344 ConfigObject fieldValues = root.toConfig().withValue(primaryField, inlinedValue).root()
345 .withoutKey(matched)
346 .withFallback(aliasDefaults);
347 return fieldValues.withValue(classField, ConfigValueFactory.fromAnyRef(
348 matched, "inlined key to type from " + root.origin().description()));
349 }
350
351
352 ConfigValue defaultObject = pluginMap.config().root().get("_default");
353 if (defaultObject != null) {
354 String defaultName = pluginMap.getLastAlias("_default");
355 ConfigObject aliasDefaults = pluginMap.aliasDefaults("_default");
356 return root.withValue(classField, ConfigValueFactory.fromAnyRef(
357 defaultName, pluginMap.category() + " default type : "
358 + defaultObject.origin().description()))
359 .withFallback(aliasDefaults);
360 }
361 }
362 return root;
363 }
364 }