2 * Copyright 2018-present Open Networking Foundation
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package io.atomix.utils.config;
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;
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;
49 import java.util.Properties;
51 import java.util.stream.Collectors;
52 import java.util.stream.Stream;
54 import static com.google.common.base.Preconditions.checkNotNull;
57 * Utility for applying Typesafe configurations to Atomix configuration objects.
59 public class ConfigMapper {
60 private static final Logger LOGGER = LoggerFactory.getLogger(ConfigMapper.class);
61 private final ClassLoader classLoader;
63 public ConfigMapper(ClassLoader classLoader) {
64 this.classLoader = classLoader;
68 * Loads the given configuration file using the mapper, falling back to the given resources.
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
76 public <T> T loadFiles(Class<T> type, List<File> files, List<String> resources) {
78 return loadResources(type, resources);
81 Config config = ConfigFactory.systemProperties();
82 for (File file : files) {
83 config = config.withFallback(ConfigFactory.parseFile(file, ConfigParseOptions.defaults().setAllowMissing(false)));
86 for (String resource : resources) {
87 config = config.withFallback(ConfigFactory.load(classLoader, resource));
89 return map(checkNotNull(config, "config cannot be null").resolve(), type);
93 * Loads the given resources using the configuration mapper.
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
100 public <T> T loadResources(Class<T> type, String... resources) {
101 return loadResources(type, Arrays.asList(resources));
105 * Loads the given resources using the configuration mapper.
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
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");
116 Config config = null;
117 for (String resource : resources) {
118 if (config == null) {
119 config = ConfigFactory.load(classLoader, resource);
121 config = config.withFallback(ConfigFactory.load(classLoader, resource));
124 return map(checkNotNull(config, "config cannot be null").resolve(), type);
128 * Applies the given configuration to the given type.
130 * @param config the configuration to apply
131 * @param clazz the class to which to apply the configuration
133 protected <T> T map(Config config, Class<T> clazz) {
134 return map(config, null, null, clazz);
137 protected <T> T newInstance(Config config, String key, Class<T> clazz) {
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);
146 * Applies the given configuration to the given type.
148 * @param config the configuration to apply
149 * @param clazz the class to which to apply the configuration
151 @SuppressWarnings("unchecked")
152 protected <T> T map(Config config, String path, String name, Class<T> clazz) {
153 T instance = newInstance(config, name, clazz);
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);
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);
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);
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 + ".")))
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));
192 private List<String> describeProperties(Object instance) {
193 Stream<String> setters = getSetterDescriptors(instance.getClass())
195 .map(descriptor -> descriptor.name);
196 Stream<String> fields = getFieldDescriptors(instance.getClass())
198 .map(descriptor -> descriptor.name);
199 return Stream.concat(setters, fields)
201 .collect(Collectors.toList());
204 private <T> void mapSetters(T instance, Class<T> clazz, String path, String name, Map<String, String> propertyNames, Config config) {
206 for (SetterDescriptor descriptor : getSetterDescriptors(instance.getClass())) {
207 Method setter = descriptor.setter;
208 Type parameterType = setter.getGenericParameterTypes()[0];
209 Class<?> parameterClass = setter.getParameterTypes()[0];
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) {
217 LOGGER.warn("{} is deprecated!", name);
219 LOGGER.warn("{}.{} is deprecated!", path, name);
222 setter.invoke(instance, name);
227 Object value = getValue(instance.getClass(), parameterType, parameterClass, config, toPath(path, name), configPropName);
229 if (descriptor.deprecated) {
231 LOGGER.warn("{}.{} is deprecated!", name, configPropName);
233 LOGGER.warn("{}.{}.{} is deprecated!", path, name, configPropName);
236 setter.invoke(instance, value);
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);
246 private <T> void mapFields(T instance, Class<T> clazz, String path, String name, Map<String, String> propertyNames, Config config) {
248 for (FieldDescriptor descriptor : getFieldDescriptors(instance.getClass())) {
249 Field field = descriptor.field;
250 field.setAccessible(true);
252 Type genericType = field.getGenericType();
253 Class<?> fieldClass = field.getType();
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);
261 field.set(instance, name);
266 Object value = getValue(instance.getClass(), genericType, fieldClass, config, toPath(path, name), configPropName);
268 if (descriptor.deprecated) {
269 LOGGER.warn("{}.{} is deprecated!", path, name);
271 field.set(instance, value);
274 } catch (IllegalAccessException e) {
275 throw new ConfigurationException(instance.getClass().getName() + " fields are not accessible, they must be for use as a bean", e);
279 protected Object getValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPath, String configPropName) {
280 if (parameterClass == Boolean.class || parameterClass == boolean.class) {
282 return config.getBoolean(configPropName);
283 } catch (ConfigException.WrongType e) {
284 return Boolean.parseBoolean(config.getString(configPropName));
286 } else if (parameterClass == Integer.class || parameterClass == int.class) {
288 return config.getInt(configPropName);
289 } catch (ConfigException.WrongType e) {
291 return Integer.parseInt(config.getString(configPropName));
292 } catch (NumberFormatException e1) {
296 } else if (parameterClass == Double.class || parameterClass == double.class) {
298 return config.getDouble(configPropName);
299 } catch (ConfigException.WrongType e) {
301 return Double.parseDouble(config.getString(configPropName));
302 } catch (NumberFormatException e1) {
306 } else if (parameterClass == Long.class || parameterClass == long.class) {
308 return config.getLong(configPropName);
309 } catch (ConfigException.WrongType e) {
311 return Long.parseLong(config.getString(configPropName));
312 } catch (NumberFormatException e1) {
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);
342 return classLoader.loadClass(className);
343 } catch (ClassNotFoundException e) {
344 throw new ConfigurationException("Failed to load class: " + className);
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);
352 Deprecated deprecated = enumValue.getDeclaringClass().getField(enumName).getAnnotation(Deprecated.class);
353 if (deprecated != null) {
354 LOGGER.warn("{}.{} = {} is deprecated!", configPath, configPropName, value);
356 } catch (NoSuchFieldException e) {
360 return map(config.getConfig(configPropName), configPath, configPropName, parameterClass);
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];
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);
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) {
391 throw new ConfigurationException("Invalid map key type: " + keyType);
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));
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();
405 if (elementType == Boolean.class) {
407 return config.getBooleanList(configPropName);
408 } catch (ConfigException.WrongType e) {
409 return config.getStringList(configPropName)
411 .map(Boolean::parseBoolean)
412 .collect(Collectors.toList());
414 } else if (elementType == Integer.class) {
416 return config.getIntList(configPropName);
417 } catch (ConfigException.WrongType e) {
418 return config.getStringList(configPropName)
422 return Integer.parseInt(value);
423 } catch (NumberFormatException e2) {
426 }).collect(Collectors.toList());
428 } else if (elementType == Double.class) {
430 return config.getDoubleList(configPropName);
431 } catch (ConfigException.WrongType e) {
432 return config.getStringList(configPropName)
436 return Double.parseDouble(value);
437 } catch (NumberFormatException e2) {
440 }).collect(Collectors.toList());
442 } else if (elementType == Long.class) {
444 return config.getLongList(configPropName);
445 } catch (ConfigException.WrongType e) {
446 return config.getStringList(configPropName)
450 return Long.parseLong(value);
451 } catch (NumberFormatException e2) {
454 }).collect(Collectors.toList());
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)
470 return classLoader.loadClass(className);
471 } catch (ClassNotFoundException e) {
472 throw new ConfigurationException("Failed to load class: " + className);
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);
483 List<Object> beanList = new ArrayList<>();
484 List<? extends Config> configList = config.getConfigList(configPropName);
486 for (Config listMember : configList) {
487 beanList.add(map(listMember, toPath(configPath, configPropName), String.valueOf(i), (Class<?>) elementType));
493 protected String toPath(String path, String name) {
494 return path != null ? String.format("%s.%s", path, name) : name;
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;
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);
519 nameBuilder.append(word.substring(0, 1).toUpperCase());
520 nameBuilder.append(word.substring(1));
523 return nameBuilder.toString();
528 protected static String toSetterName(String name) {
529 return "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
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
538 && "set".equals(name.substring(0, 3))
539 && name.charAt(3) >= 'A'
540 && name.charAt(3) <= 'Z') {
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();
548 // Strip the "Config" suffix from the property name.
549 if (name.endsWith("Config")) {
550 name = name.substring(0, name.length() - "Config".length());
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));
562 descriptors.put(name, new SetterDescriptor(name, method));
566 return descriptors.values();
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())) {
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())))
584 if (method != null) {
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());
593 descriptors.putIfAbsent(name, new FieldDescriptor(name, field));
595 clazz = clazz.getSuperclass();
597 return Lists.newArrayList(descriptors.values());
600 protected static class SetterDescriptor {
601 private final String name;
602 private final Method setter;
603 private final boolean deprecated;
605 SetterDescriptor(String name, Method setter) {
607 this.setter = setter;
608 this.deprecated = setter.getAnnotation(Deprecated.class) != null;
612 protected static class FieldDescriptor {
613 private final String name;
614 private final Field field;
615 private final boolean deprecated;
617 FieldDescriptor(String name, Field field) {
620 this.deprecated = field.getAnnotation(Deprecated.class) != null;