View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
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       * Construct an object of the requested type based on the default values and types (if the requested
54       * is not a concrete class).
55       */
56      public static <T> T newDefault(@Nonnull Class<T> type) throws IOException {
57          return Jackson.defaultCodec().newDefault(type);
58      }
59  
60      /** Construct an object of the requested plugin category based on the default type and values */
61      public static <T> T newDefault(@Nonnull String category) throws IOException {
62          return Jackson.defaultCodec().newDefault(category);
63      }
64  
65      /**
66       * Instantiate an object of the requested type based on the provided config. The config should only contain
67       * field and type information for the object to be constructed. Global defaults, plugin configuration, etc, are
68       * provided by this CodecConfig instance's globalConfig and pluginRegistry fields.
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       * Instantiate an object of the requested category based on the provided config. The config should only contain
76       * field and type information for the object to be constructed. Global defaults, plugin configuration, etc, are
77       * provided by this CodecConfig instance's globalConfig and pluginRegistry fields.
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       * Tries to parse the string as an isolated typesafe-config object, tries to resolve it, and then calls
85       * {@link #decodeObject(Class, Config)} with the resultant config and the passed in type. Pretty much just
86       * a convenience function for simple use cases that don't want to care about how ConfigFactory works.
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       * Tries to parse the string as an isolated typesafe-config object, tries to resolve it, and then calls
95       * {@link #decodeObject(String, Config)} with the resultant config and the passed in category. Pretty much just
96       * a convenience function for simple use cases that don't want to care about how ConfigFactory works.
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      * Instantiate an object without a compile time expected type. This expects a config of the
105      * form "{plugin-category: {...}}". ie. there should be exactly one top level key and that
106      * key should be a valid, loaded, plug-in category.
107      */
108     public static <T> T decodeObject(@Syntax("HOCON") String configText) throws IOException {
109         return Jackson.defaultCodec().decodeObject(configText);
110     }
111 
112     /**
113      * Instantiate an object without a compile time expected type. This expects a config of the
114      * form "{plugin-category: {...}}". ie. there should be exactly one top level key and that
115      * key should be a valid, loaded, plug-in category.
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                 // try not to throw exceptions or at least checked exceptions from this helper method
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             // primitive numeric types are not subclasses of Number, so just catch all non-booleans
263             return false;
264         } else if (expectedType.isEnum()) {
265             return false;
266         } else {
267             return true;
268         }
269     }
270 
271     /** should be roughly analagous to a removed method */
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         // normal, explicit typing
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             // single key as type
305             if (root.size() == 1) {
306                 String onlyKey = root.keySet().iterator().next();
307                 try {
308                     pluginMap.getClass(onlyKey); // make sure key is a valid type
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             // inlined type
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             // default type
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 }