Import atomix/{storage,utils}
[controller.git] / third-party / atomix / utils / src / main / java / io / atomix / utils / config / ConfigMapper.java
1 /*
2  * Copyright 2018-present Open Networking Foundation
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package io.atomix.utils.config;
17
18 import com.google.common.base.Joiner;
19 import com.google.common.collect.Lists;
20 import com.google.common.collect.Maps;
21 import com.typesafe.config.Config;
22 import com.typesafe.config.ConfigException;
23 import com.typesafe.config.ConfigFactory;
24 import com.typesafe.config.ConfigList;
25 import com.typesafe.config.ConfigMemorySize;
26 import com.typesafe.config.ConfigObject;
27 import com.typesafe.config.ConfigParseOptions;
28 import com.typesafe.config.ConfigValue;
29 import io.atomix.utils.Named;
30 import io.atomix.utils.memory.MemorySize;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 import java.io.File;
35 import java.lang.reflect.Field;
36 import java.lang.reflect.InvocationTargetException;
37 import java.lang.reflect.Method;
38 import java.lang.reflect.Modifier;
39 import java.lang.reflect.ParameterizedType;
40 import java.lang.reflect.Type;
41 import java.time.Duration;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.Collection;
45 import java.util.HashMap;
46 import java.util.HashSet;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Properties;
50 import java.util.Set;
51 import java.util.stream.Collectors;
52 import java.util.stream.Stream;
53
54 import static com.google.common.base.Preconditions.checkNotNull;
55
56 /**
57  * Utility for applying Typesafe configurations to Atomix configuration objects.
58  */
59 public class ConfigMapper {
60   private static final Logger LOGGER = LoggerFactory.getLogger(ConfigMapper.class);
61   private final ClassLoader classLoader;
62
63   public ConfigMapper(ClassLoader classLoader) {
64     this.classLoader = classLoader;
65   }
66
67   /**
68    * Loads the given configuration file using the mapper, falling back to the given resources.
69    *
70    * @param type      the type to load
71    * @param files     the files to load
72    * @param resources the resources to which to fall back
73    * @param <T>       the resulting type
74    * @return the loaded configuration
75    */
76   public <T> T loadFiles(Class<T> type, List<File> files, List<String> resources) {
77     if (files == null) {
78       return loadResources(type, resources);
79     }
80
81     Config config = ConfigFactory.systemProperties();
82     for (File file : files) {
83       config = config.withFallback(ConfigFactory.parseFile(file, ConfigParseOptions.defaults().setAllowMissing(false)));
84     }
85
86     for (String resource : resources) {
87       config = config.withFallback(ConfigFactory.load(classLoader, resource));
88     }
89     return map(checkNotNull(config, "config cannot be null").resolve(), type);
90   }
91
92   /**
93    * Loads the given resources using the configuration mapper.
94    *
95    * @param type      the type to load
96    * @param resources the resources to load
97    * @param <T>       the resulting type
98    * @return the loaded configuration
99    */
100   public <T> T loadResources(Class<T> type, String... resources) {
101     return loadResources(type, Arrays.asList(resources));
102   }
103
104   /**
105    * Loads the given resources using the configuration mapper.
106    *
107    * @param type      the type to load
108    * @param resources the resources to load
109    * @param <T>       the resulting type
110    * @return the loaded configuration
111    */
112   public <T> T loadResources(Class<T> type, List<String> resources) {
113     if (resources == null || resources.isEmpty()) {
114       throw new IllegalArgumentException("resources must be defined");
115     }
116     Config config = null;
117     for (String resource : resources) {
118       if (config == null) {
119         config = ConfigFactory.load(classLoader, resource);
120       } else {
121         config = config.withFallback(ConfigFactory.load(classLoader, resource));
122       }
123     }
124     return map(checkNotNull(config, "config cannot be null").resolve(), type);
125   }
126
127   /**
128    * Applies the given configuration to the given type.
129    *
130    * @param config the configuration to apply
131    * @param clazz  the class to which to apply the configuration
132    */
133   protected <T> T map(Config config, Class<T> clazz) {
134     return map(config, null, null, clazz);
135   }
136
137   protected <T> T newInstance(Config config, String key, Class<T> clazz) {
138     try {
139       return clazz.newInstance();
140     } catch (InstantiationException | IllegalAccessException e) {
141       throw new ConfigurationException(clazz.getName() + " needs a public no-args constructor to be used as a bean", e);
142     }
143   }
144
145   /**
146    * Applies the given configuration to the given type.
147    *
148    * @param config the configuration to apply
149    * @param clazz  the class to which to apply the configuration
150    */
151   @SuppressWarnings("unchecked")
152   protected <T> T map(Config config, String path, String name, Class<T> clazz) {
153     T instance = newInstance(config, name, clazz);
154
155     // Map config property names to bean properties.
156     Map<String, String> propertyNames = new HashMap<>();
157     for (Map.Entry<String, ConfigValue> configProp : config.root().entrySet()) {
158       String originalName = configProp.getKey();
159       String camelName = toCamelCase(originalName);
160       // if a setting is in there both as some hyphen name and the camel name,
161       // the camel one wins
162       if (!propertyNames.containsKey(camelName) || originalName.equals(camelName)) {
163         propertyNames.put(camelName, originalName);
164       }
165     }
166
167     // First use setters and then fall back to fields.
168     mapSetters(instance, clazz, path, name, propertyNames, config);
169     mapFields(instance, clazz, path, name, propertyNames, config);
170
171     // If any properties present in the configuration were not found on config beans, throw an exception.
172     if (!propertyNames.isEmpty()) {
173       checkRemainingProperties(propertyNames.keySet(), describeProperties(instance), toPath(path, name), clazz);
174     }
175     return instance;
176   }
177
178   protected void checkRemainingProperties(Set<String> missingProperties, List<String> availableProperties, String path, Class<?> clazz) {
179     Properties properties = System.getProperties();
180     List<String> cleanNames = missingProperties.stream()
181         .map(propertyName -> toPath(path, propertyName))
182         .filter(propertyName -> !properties.containsKey(propertyName))
183         .filter(propertyName -> properties.entrySet().stream().noneMatch(entry -> entry.getKey().toString().startsWith(propertyName + ".")))
184         .sorted()
185         .collect(Collectors.toList());
186     if (!cleanNames.isEmpty()) {
187       throw new ConfigurationException("Unknown properties present in configuration: " + Joiner.on(", ").join(cleanNames) + "\n"
188           + "Available properties:\n- " + Joiner.on("\n- ").join(availableProperties));
189     }
190   }
191
192   private List<String> describeProperties(Object instance) {
193     Stream<String> setters = getSetterDescriptors(instance.getClass())
194         .stream()
195         .map(descriptor -> descriptor.name);
196     Stream<String> fields = getFieldDescriptors(instance.getClass())
197         .stream()
198         .map(descriptor -> descriptor.name);
199     return Stream.concat(setters, fields)
200         .sorted()
201         .collect(Collectors.toList());
202   }
203
204   private <T> void mapSetters(T instance, Class<T> clazz, String path, String name, Map<String, String> propertyNames, Config config) {
205     try {
206       for (SetterDescriptor descriptor : getSetterDescriptors(instance.getClass())) {
207         Method setter = descriptor.setter;
208         Type parameterType = setter.getGenericParameterTypes()[0];
209         Class<?> parameterClass = setter.getParameterTypes()[0];
210
211         String configPropName = propertyNames.remove(descriptor.name);
212         if (configPropName == null) {
213           if ((Named.class.isAssignableFrom(clazz) || NamedConfig.class.isAssignableFrom(clazz))
214               && descriptor.setter.getParameterTypes()[0] == String.class && name != null && "name".equals(descriptor.name)) {
215             if (descriptor.deprecated) {
216               if (path == null) {
217                 LOGGER.warn("{} is deprecated!", name);
218               } else {
219                 LOGGER.warn("{}.{} is deprecated!", path, name);
220               }
221             }
222             setter.invoke(instance, name);
223           }
224           continue;
225         }
226
227         Object value = getValue(instance.getClass(), parameterType, parameterClass, config, toPath(path, name), configPropName);
228         if (value != null) {
229           if (descriptor.deprecated) {
230             if (path == null) {
231               LOGGER.warn("{}.{} is deprecated!", name, configPropName);
232             } else {
233               LOGGER.warn("{}.{}.{} is deprecated!", path, name, configPropName);
234             }
235           }
236           setter.invoke(instance, value);
237         }
238       }
239     } catch (IllegalAccessException e) {
240       throw new ConfigurationException(instance.getClass().getName() + " getters and setters are not accessible, they must be for use as a bean", e);
241     } catch (InvocationTargetException e) {
242       throw new ConfigurationException("Calling bean method on " + instance.getClass().getName() + " caused an exception", e);
243     }
244   }
245
246   private <T> void mapFields(T instance, Class<T> clazz, String path, String name, Map<String, String> propertyNames, Config config) {
247     try {
248       for (FieldDescriptor descriptor : getFieldDescriptors(instance.getClass())) {
249         Field field = descriptor.field;
250         field.setAccessible(true);
251
252         Type genericType = field.getGenericType();
253         Class<?> fieldClass = field.getType();
254
255         String configPropName = propertyNames.remove(descriptor.name);
256         if (configPropName == null) {
257           if (Named.class.isAssignableFrom(clazz) && field.getType() == String.class && name != null && "name".equals(descriptor.name)) {
258             if (descriptor.deprecated) {
259               LOGGER.warn("{}.{} is deprecated!", path, name);
260             }
261             field.set(instance, name);
262           }
263           continue;
264         }
265
266         Object value = getValue(instance.getClass(), genericType, fieldClass, config, toPath(path, name), configPropName);
267         if (value != null) {
268           if (descriptor.deprecated) {
269             LOGGER.warn("{}.{} is deprecated!", path, name);
270           }
271           field.set(instance, value);
272         }
273       }
274     } catch (IllegalAccessException e) {
275       throw new ConfigurationException(instance.getClass().getName() + " fields are not accessible, they must be for use as a bean", e);
276     }
277   }
278
279   protected Object getValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPath, String configPropName) {
280     if (parameterClass == Boolean.class || parameterClass == boolean.class) {
281       try {
282         return config.getBoolean(configPropName);
283       } catch (ConfigException.WrongType e) {
284         return Boolean.parseBoolean(config.getString(configPropName));
285       }
286     } else if (parameterClass == Integer.class || parameterClass == int.class) {
287       try {
288         return config.getInt(configPropName);
289       } catch (ConfigException.WrongType e) {
290         try {
291           return Integer.parseInt(config.getString(configPropName));
292         } catch (NumberFormatException e1) {
293           throw e;
294         }
295       }
296     } else if (parameterClass == Double.class || parameterClass == double.class) {
297       try {
298         return config.getDouble(configPropName);
299       } catch (ConfigException.WrongType e) {
300         try {
301           return Double.parseDouble(config.getString(configPropName));
302         } catch (NumberFormatException e1) {
303           throw e;
304         }
305       }
306     } else if (parameterClass == Long.class || parameterClass == long.class) {
307       try {
308         return config.getLong(configPropName);
309       } catch (ConfigException.WrongType e) {
310         try {
311           return Long.parseLong(config.getString(configPropName));
312         } catch (NumberFormatException e1) {
313           throw e;
314         }
315       }
316     } else if (parameterClass == String.class) {
317       return config.getString(configPropName);
318     } else if (parameterClass == Duration.class) {
319       return config.getDuration(configPropName);
320     } else if (parameterClass == MemorySize.class) {
321       ConfigMemorySize size = config.getMemorySize(configPropName);
322       return new MemorySize(size.toBytes());
323     } else if (parameterClass == Object.class) {
324       return config.getAnyRef(configPropName);
325     } else if (parameterClass == List.class || parameterClass == Collection.class) {
326       return getListValue(beanClass, parameterType, parameterClass, config, configPath, configPropName);
327     } else if (parameterClass == Set.class) {
328       return getSetValue(beanClass, parameterType, parameterClass, config, configPath, configPropName);
329     } else if (parameterClass == Map.class) {
330       return getMapValue(beanClass, parameterType, parameterClass, config, configPath, configPropName);
331     } else if (parameterClass == Config.class) {
332       return config.getConfig(configPropName);
333     } else if (parameterClass == ConfigObject.class) {
334       return config.getObject(configPropName);
335     } else if (parameterClass == ConfigValue.class) {
336       return config.getValue(configPropName);
337     } else if (parameterClass == ConfigList.class) {
338       return config.getList(configPropName);
339     } else if (parameterClass == Class.class) {
340       String className = config.getString(configPropName);
341       try {
342         return classLoader.loadClass(className);
343       } catch (ClassNotFoundException e) {
344         throw new ConfigurationException("Failed to load class: " + className);
345       }
346     } else if (parameterClass.isEnum()) {
347       String value = config.getString(configPropName);
348       String enumName = value.replace("-", "_").toUpperCase();
349       @SuppressWarnings("unchecked")
350       Enum enumValue = Enum.valueOf((Class<Enum>) parameterClass, enumName);
351       try {
352         Deprecated deprecated = enumValue.getDeclaringClass().getField(enumName).getAnnotation(Deprecated.class);
353         if (deprecated != null) {
354           LOGGER.warn("{}.{} = {} is deprecated!", configPath, configPropName, value);
355         }
356       } catch (NoSuchFieldException e) {
357       }
358       return enumValue;
359     } else {
360       return map(config.getConfig(configPropName), configPath, configPropName, parameterClass);
361     }
362   }
363
364   protected Map getMapValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPath, String configPropName) {
365     Type[] typeArgs = ((ParameterizedType) parameterType).getActualTypeArguments();
366     Type keyType = typeArgs[0];
367     Type valueType = typeArgs[1];
368
369     Map<Object, Object> map = new HashMap<>();
370     Config childConfig = config.getConfig(configPropName);
371     Class valueClass = (Class) (valueType instanceof ParameterizedType ? ((ParameterizedType) valueType).getRawType() : valueType);
372     for (String key : config.getObject(configPropName).unwrapped().keySet()) {
373       Object value = getValue(Map.class, valueType, valueClass, childConfig, toPath(configPath, configPropName), key);
374       map.put(getKeyValue(keyType, key), value);
375     }
376     return map;
377   }
378
379   protected Object getKeyValue(Type keyType, String key) {
380     if (keyType == Boolean.class || keyType == boolean.class) {
381       return Boolean.parseBoolean(key);
382     } else if (keyType == Integer.class || keyType == int.class) {
383       return Integer.parseInt(key);
384     } else if (keyType == Double.class || keyType == double.class) {
385       return Double.parseDouble(key);
386     } else if (keyType == Long.class || keyType == long.class) {
387       return Long.parseLong(key);
388     } else if (keyType == String.class) {
389       return key;
390     } else {
391       throw new ConfigurationException("Invalid map key type: " + keyType);
392     }
393   }
394
395   protected Object getSetValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPath, String configPropName) {
396     return new HashSet((List) getListValue(beanClass, parameterType, parameterClass, config, configPath, configPropName));
397   }
398
399   protected Object getListValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPath, String configPropName) {
400     Type elementType = ((ParameterizedType) parameterType).getActualTypeArguments()[0];
401     if (elementType instanceof ParameterizedType) {
402       elementType = ((ParameterizedType) elementType).getRawType();
403     }
404
405     if (elementType == Boolean.class) {
406       try {
407         return config.getBooleanList(configPropName);
408       } catch (ConfigException.WrongType e) {
409         return config.getStringList(configPropName)
410             .stream()
411             .map(Boolean::parseBoolean)
412             .collect(Collectors.toList());
413       }
414     } else if (elementType == Integer.class) {
415       try {
416         return config.getIntList(configPropName);
417       } catch (ConfigException.WrongType e) {
418         return config.getStringList(configPropName)
419             .stream()
420             .map(value -> {
421               try {
422                 return Integer.parseInt(value);
423               } catch (NumberFormatException e2) {
424                 throw e;
425               }
426             }).collect(Collectors.toList());
427       }
428     } else if (elementType == Double.class) {
429       try {
430         return config.getDoubleList(configPropName);
431       } catch (ConfigException.WrongType e) {
432         return config.getStringList(configPropName)
433             .stream()
434             .map(value -> {
435               try {
436                 return Double.parseDouble(value);
437               } catch (NumberFormatException e2) {
438                 throw e;
439               }
440             }).collect(Collectors.toList());
441       }
442     } else if (elementType == Long.class) {
443       try {
444         return config.getLongList(configPropName);
445       } catch (ConfigException.WrongType e) {
446         return config.getStringList(configPropName)
447             .stream()
448             .map(value -> {
449               try {
450                 return Long.parseLong(value);
451               } catch (NumberFormatException e2) {
452                 throw e;
453               }
454             }).collect(Collectors.toList());
455       }
456     } else if (elementType == String.class) {
457       return config.getStringList(configPropName);
458     } else if (elementType == Duration.class) {
459       return config.getDurationList(configPropName);
460     } else if (elementType == MemorySize.class) {
461       List<ConfigMemorySize> sizes = config.getMemorySizeList(configPropName);
462       return sizes.stream()
463           .map(size -> new MemorySize(size.toBytes()))
464           .collect(Collectors.toList());
465     } else if (elementType == Class.class) {
466       return config.getStringList(configPropName)
467           .stream()
468           .map(className -> {
469             try {
470               return classLoader.loadClass(className);
471             } catch (ClassNotFoundException e) {
472               throw new ConfigurationException("Failed to load class: " + className);
473             }
474           })
475           .collect(Collectors.toList());
476     } else if (elementType == Object.class) {
477       return config.getAnyRefList(configPropName);
478     } else if (((Class<?>) elementType).isEnum()) {
479       @SuppressWarnings("unchecked")
480       List<Enum> enumValues = config.getEnumList((Class<Enum>) elementType, configPropName);
481       return enumValues;
482     } else {
483       List<Object> beanList = new ArrayList<>();
484       List<? extends Config> configList = config.getConfigList(configPropName);
485       int i = 0;
486       for (Config listMember : configList) {
487         beanList.add(map(listMember, toPath(configPath, configPropName), String.valueOf(i), (Class<?>) elementType));
488       }
489       return beanList;
490     }
491   }
492
493   protected String toPath(String path, String name) {
494     return path != null ? String.format("%s.%s", path, name) : name;
495   }
496
497   protected static boolean isSimpleType(Class<?> parameterClass) {
498     return parameterClass == Boolean.class || parameterClass == boolean.class
499         || parameterClass == Integer.class || parameterClass == int.class
500         || parameterClass == Double.class || parameterClass == double.class
501         || parameterClass == Long.class || parameterClass == long.class
502         || parameterClass == String.class
503         || parameterClass == Duration.class
504         || parameterClass == MemorySize.class
505         || parameterClass == List.class
506         || parameterClass == Map.class
507         || parameterClass == Class.class;
508   }
509
510   protected static String toCamelCase(String originalName) {
511     String[] words = originalName.split("-+");
512     if (words.length > 1) {
513       LOGGER.warn("Kebab case config name '" + originalName + "' is deprecated!");
514       StringBuilder nameBuilder = new StringBuilder(originalName.length());
515       for (String word : words) {
516         if (nameBuilder.length() == 0) {
517           nameBuilder.append(word);
518         } else {
519           nameBuilder.append(word.substring(0, 1).toUpperCase());
520           nameBuilder.append(word.substring(1));
521         }
522       }
523       return nameBuilder.toString();
524     }
525     return originalName;
526   }
527
528   protected static String toSetterName(String name) {
529     return "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
530   }
531
532   protected static Collection<SetterDescriptor> getSetterDescriptors(Class<?> clazz) {
533     Map<String, SetterDescriptor> descriptors = Maps.newHashMap();
534     for (Method method : clazz.getMethods()) {
535       String name = method.getName();
536       if (method.getParameterTypes().length == 1
537           && name.length() > 3
538           && "set".equals(name.substring(0, 3))
539           && name.charAt(3) >= 'A'
540           && name.charAt(3) <= 'Z') {
541
542         // Strip the "set" prefix from the property name.
543         name = method.getName().substring(3);
544         name = name.length() > 1
545             ? name.substring(0, 1).toLowerCase() + name.substring(1)
546             : name.toLowerCase();
547
548         // Strip the "Config" suffix from the property name.
549         if (name.endsWith("Config")) {
550           name = name.substring(0, name.length() - "Config".length());
551         }
552
553         // If a setter with this property name has already been registered, determine whether to override it.
554         // We favor simpler types over more complex types (i.e. beans).
555         SetterDescriptor descriptor = descriptors.get(name);
556         if (descriptor != null) {
557           Class<?> type = method.getParameterTypes()[0];
558           if (isSimpleType(type)) {
559             descriptors.put(name, new SetterDescriptor(name, method));
560           }
561         } else {
562           descriptors.put(name, new SetterDescriptor(name, method));
563         }
564       }
565     }
566     return descriptors.values();
567   }
568
569   protected static Collection<FieldDescriptor> getFieldDescriptors(Class<?> type) {
570     Class<?> clazz = type;
571     Map<String, FieldDescriptor> descriptors = Maps.newHashMap();
572     while (clazz != Object.class) {
573       for (Field field : clazz.getDeclaredFields()) {
574         // If the field is static or transient, ignore it.
575         if (Modifier.isTransient(field.getModifiers()) || Modifier.isStatic(field.getModifiers())) {
576           continue;
577         }
578
579         // If the field has a setter, ignore it and use the setter.
580         Method method = Stream.of(clazz.getMethods())
581             .filter(m -> m.getName().equals(toSetterName(field.getName())))
582             .findFirst()
583             .orElse(null);
584         if (method != null) {
585           continue;
586         }
587
588         // Strip the "Config" suffix from the field.
589         String name = field.getName();
590         if (name.endsWith("Config")) {
591           name = name.substring(0, name.length() - "Config".length());
592         }
593         descriptors.putIfAbsent(name, new FieldDescriptor(name, field));
594       }
595       clazz = clazz.getSuperclass();
596     }
597     return Lists.newArrayList(descriptors.values());
598   }
599
600   protected static class SetterDescriptor {
601     private final String name;
602     private final Method setter;
603     private final boolean deprecated;
604
605     SetterDescriptor(String name, Method setter) {
606       this.name = name;
607       this.setter = setter;
608       this.deprecated = setter.getAnnotation(Deprecated.class) != null;
609     }
610   }
611
612   protected static class FieldDescriptor {
613     private final String name;
614     private final Field field;
615     private final boolean deprecated;
616
617     FieldDescriptor(String name, Field field) {
618       this.name = name;
619       this.field = field;
620       this.deprecated = field.getAnnotation(Deprecated.class) != null;
621     }
622   }
623 }