Java tutorial
/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.addthis.codec.config; import javax.annotation.Nonnull; import javax.annotation.Syntax; import java.io.IOException; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import com.addthis.codec.jackson.Jackson; import com.addthis.codec.plugins.PluginMap; import com.addthis.codec.plugins.PluginRegistry; import com.addthis.codec.reflection.CodableClassInfo; import com.addthis.codec.reflection.CodableFieldInfo; import com.google.common.annotations.Beta; import com.google.common.base.Objects; import com.fasterxml.jackson.core.type.TypeReference; import com.typesafe.config.Config; import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigList; import com.typesafe.config.ConfigObject; import com.typesafe.config.ConfigValue; import com.typesafe.config.ConfigValueFactory; import com.typesafe.config.ConfigValueType; @Beta public final class Configs { private Configs() { } /** * Construct an object of the requested type based on the default values and types (if the requested * is not a concrete class). */ public static <T> T newDefault(@Nonnull Class<T> type) throws IOException { return Jackson.defaultCodec().newDefault(type); } /** Construct an object of the requested plugin category based on the default type and values */ public static <T> T newDefault(@Nonnull String category) throws IOException { return Jackson.defaultCodec().newDefault(category); } /** * Instantiate an object of the requested type based on the provided config. The config should only contain * field and type information for the object to be constructed. Global defaults, plugin configuration, etc, are * provided by this CodecConfig instance's globalConfig and pluginRegistry fields. */ public static <T> T decodeObject(@Nonnull Class<T> type, @Nonnull Config config) throws IOException { return Jackson.defaultCodec().decodeObject(type, config); } /** * Instantiate an object of the requested type based on the provided config. The config should only contain * field and type information for the object to be constructed. Global defaults, plugin configuration, etc, are * provided by this CodecConfig instance's globalConfig and pluginRegistry fields. */ public static <T> T decodeObject(@Nonnull TypeReference<T> type, @Nonnull Config config) throws IOException { return Jackson.defaultCodec().decodeObject(type, config); } /** * Instantiate an object of the requested category based on the provided config. The config should only contain * field and type information for the object to be constructed. Global defaults, plugin configuration, etc, are * provided by this CodecConfig instance's globalConfig and pluginRegistry fields. */ public static <T> T decodeObject(@Nonnull String category, @Nonnull Config config) throws IOException { return Jackson.defaultCodec().decodeObject(category, config); } /** * Tries to parse the string as an isolated typesafe-config object, tries to resolve it, and then calls * {@link #decodeObject(Class, Config)} with the resultant config and the passed in type. Pretty much just * a convenience function for simple use cases that don't want to care about how ConfigFactory works. */ public static <T> T decodeObject(@Nonnull Class<T> type, @Syntax("HOCON") @Nonnull String configText) throws IOException { return Jackson.defaultCodec().decodeObject(type, configText); } /** * Tries to parse the string as an isolated typesafe-config object, tries to resolve it, and then calls * {@link #decodeObject(TypeReference, Config)} with the resultant config and the passed in type. Pretty much just * a convenience function for simple use cases that don't want to care about how ConfigFactory works. */ public static <T> T decodeObject(@Nonnull TypeReference<T> type, @Syntax("HOCON") @Nonnull String configText) throws IOException { return Jackson.defaultCodec().decodeObject(type, configText); } /** * Tries to parse the string as an isolated typesafe-config object, tries to resolve it, and then calls * {@link #decodeObject(String, Config)} with the resultant config and the passed in category. Pretty much just * a convenience function for simple use cases that don't want to care about how ConfigFactory works. */ public static <T> T decodeObject(@Nonnull String category, @Syntax("HOCON") @Nonnull String configText) throws IOException { return Jackson.defaultCodec().decodeObject(category, configText); } /** * Instantiate an object without a compile time expected type. This expects a config of the * form "{plugin-category: {...}}". ie. there should be exactly one top level key and that * key should be a valid, loaded, plug-in category. */ public static <T> T decodeObject(@Syntax("HOCON") String configText) throws IOException { return Jackson.defaultCodec().decodeObject(configText); } /** * Instantiate an object without a compile time expected type. This expects a config of the * form "{plugin-category: {...}}". ie. there should be exactly one top level key and that * key should be a valid, loaded, plug-in category. */ public static <T> T decodeObject(Config config) throws IOException { return Jackson.defaultCodec().decodeObject(config); } public static ConfigValue expandSugar(Config config, PluginRegistry pluginRegistry) { if (config.root().size() != 1) { throw new ConfigException.Parse(config.root().origin(), "config root must have exactly one key"); } String category = config.root().keySet().iterator().next(); PluginMap pluginMap = pluginRegistry.asMap().get(category); if (pluginMap == null) { throw new ConfigException.BadValue(config.root().get(category).origin(), category, "top level key must be a valid category"); } Class<?> baseClass = Objects.firstNonNull(pluginMap.baseClass(), Object.class); return expandSugar(baseClass, config.root().get(category), pluginRegistry); } public static ConfigValue expandSugar(Class<?> type, ConfigValue config, PluginRegistry pluginRegistry) { if ((type != null) && type.isAssignableFrom(ConfigValue.class)) { return ConfigValueFactory.fromAnyRef(config.unwrapped(), "unchanged for raw ConfigValue field " + config.origin().description()); } CodableClassInfo typeInfo = new CodableClassInfo(type, pluginRegistry.config(), pluginRegistry); PluginMap pluginMap = typeInfo.getPluginMap(); ConfigValue valueOrResolvedRoot = resolveType(type, config, pluginMap); if (valueOrResolvedRoot.valueType() != ConfigValueType.OBJECT) { return valueOrResolvedRoot; } ConfigObject root = (ConfigObject) valueOrResolvedRoot; String classField = pluginMap.classField(); if (root.get(classField) != null) { try { type = pluginMap.getClass((String) root.get(classField).unwrapped()); } catch (ClassNotFoundException ignored) { // try not to throw exceptions or at least checked exceptions from this helper method } } return expandSugarSkipResolve(type, root, pluginRegistry); } private static ConfigObject expandSugarSkipResolve(Class<?> type, ConfigObject root, PluginRegistry pluginRegistry) { CodableClassInfo resolvedTypeInfo = new CodableClassInfo(type, pluginRegistry.config(), pluginRegistry); ConfigObject fieldDefaults = resolvedTypeInfo.getFieldDefaults().root(); for (CodableFieldInfo fieldInfo : resolvedTypeInfo.values()) { String fieldName = fieldInfo.getName(); ConfigValue fieldValue = root.get(fieldName); if ((fieldValue == null) && (fieldDefaults.get(fieldName) != null)) { ConfigValue fieldDefault = fieldDefaults.get(fieldName); fieldValue = ConfigValueFactory.fromAnyRef(fieldDefault.unwrapped(), "global default : " + fieldDefault.origin().description()); root = root.withValue(fieldName, fieldValue); } if (fieldValue == null) { continue; } if ((fieldInfo.isArray() || fieldInfo.isCollection()) && (fieldValue.valueType() != ConfigValueType.LIST)) { fieldValue = ConfigValueFactory.fromIterable(Collections.singletonList(fieldValue.unwrapped()), "auto collection of " + fieldValue.origin().description()); root = root.withValue(fieldName, fieldValue); } if (!isCodableType(fieldInfo)) { continue; } if (fieldInfo.isArray() || fieldInfo.isCollection()) { if (fieldValue.valueType() != ConfigValueType.LIST) { throw new ConfigException.WrongType(fieldValue.origin(), fieldName, ConfigValueType.LIST.name(), fieldValue.valueType().name()); } Class<?> elementType = elementType(fieldInfo); boolean nested = fieldInfo.isCollectionArray(); fieldValue = expandSugarArray(fieldValue, elementType, pluginRegistry, nested); } else if (fieldInfo.isMap()) { if (fieldValue.valueType() != ConfigValueType.OBJECT) { throw new ConfigException.WrongType(fieldValue.origin(), fieldName, ConfigValueType.OBJECT.name(), fieldValue.valueType().name()); } Class<?> elementType = elementType(fieldInfo); boolean nested = fieldInfo.isMapValueArray(); ConfigObject fieldMap = (ConfigObject) fieldValue; Map<String, Object> newMap = new HashMap<>(fieldMap.size()); for (Map.Entry<String, ConfigValue> mapEntry : fieldMap.entrySet()) { ConfigValue mapValue = mapEntry.getValue(); String mapKey = mapEntry.getKey(); ConfigValue resolvedMapObject; if (nested) { resolvedMapObject = expandSugarArray(mapValue, elementType, pluginRegistry, false); } else { resolvedMapObject = expandSugar(elementType, mapValue, pluginRegistry); } newMap.put(mapKey, resolvedMapObject.unwrapped()); } fieldValue = ConfigValueFactory.fromMap(newMap, fieldMap.origin().description()); } else { fieldValue = expandSugar(fieldInfo.getTypeOrComponentType(), fieldValue, pluginRegistry); } root = root.withValue(fieldName, fieldValue); } return root; } private static ConfigList expandSugarArray(ConfigValue fieldValue, Class<?> elementType, PluginRegistry pluginRegistry, boolean nested) { ConfigList fieldList = (ConfigList) fieldValue; List<Object> newList = new ArrayList<>(fieldList.size()); for (ConfigValue listEntry : fieldList) { ConfigValue listObject; if (nested) { listObject = expandSugarArray(listEntry, elementType, pluginRegistry, false); } else { listObject = expandSugar(elementType, listEntry, pluginRegistry); } newList.add(listObject.unwrapped()); } return ConfigValueFactory.fromIterable(newList, fieldList.origin().description()); } private static Class<?> elementType(CodableFieldInfo fieldInfo) { Class<?> elementType = fieldInfo.getTypeOrComponentType(); if (fieldInfo.isMap()) { elementType = fieldInfo.getMapValueClass(); } else if (fieldInfo.isCollection()) { elementType = fieldInfo.getCollectionClass(); } return elementType; } private static boolean isCodableType(CodableFieldInfo fieldInfo) { Class<?> expectedType = elementType(fieldInfo); if (expectedType.isAssignableFrom(String.class)) { return false; } else if ((expectedType == boolean.class) || (expectedType == Boolean.class)) { return false; } else if (expectedType == AtomicBoolean.class) { return false; } else if (Number.class.isAssignableFrom(expectedType) || expectedType.isPrimitive()) { // primitive numeric types are not subclasses of Number, so just catch all non-booleans return false; } else if (expectedType.isEnum()) { return false; } else { return true; } } /** should be roughly analagous to a removed method */ private static ConfigValue resolveType(Class<?> type, ConfigValue configValue, PluginMap pluginMap) { String classField = pluginMap.classField(); if (configValue.valueType() != ConfigValueType.OBJECT) { if ((type == null) || Modifier.isAbstract(type.getModifiers()) || Modifier.isInterface(type.getModifiers())) { if (configValue.valueType() == ConfigValueType.LIST) { Class<?> arrayType = pluginMap.arraySugar(); if (arrayType != null) { ConfigObject aliasDefaults = pluginMap.aliasDefaults("_array"); String arrayFieldName = aliasDefaults.toConfig().getString("_primary"); String arraySugarName = pluginMap.getLastAlias("_array"); return ConfigFactory.empty().root() .withValue(classField, ConfigValueFactory.fromAnyRef(arraySugarName, pluginMap.category() + " array sugar : " + pluginMap.config().root().get("_array").origin().description())) .withValue(arrayFieldName, configValue).withFallback(aliasDefaults); } } } return configValue; } ConfigObject root = (ConfigObject) configValue; ConfigValue classValue = root.get(classField); // normal, explicit typing if ((classValue != null) && (classValue.valueType() == ConfigValueType.STRING)) { String classValueString = (String) classValue.unwrapped(); ConfigObject aliasDefaults = pluginMap.aliasDefaults(classValueString); return root.withFallback(aliasDefaults); } if ((type == null) || Modifier.isAbstract(type.getModifiers()) || Modifier.isInterface(type.getModifiers())) { // single key as type if (root.size() == 1) { String onlyKey = root.keySet().iterator().next(); try { pluginMap.getClass(onlyKey); // make sure key is a valid type ConfigValue onlyKeyValue = root.values().iterator().next(); ConfigObject aliasDefaults = pluginMap.aliasDefaults(onlyKey); if (onlyKeyValue.valueType() != ConfigValueType.OBJECT) { if (aliasDefaults.get("_primary") != null) { onlyKeyValue = onlyKeyValue.atPath((String) aliasDefaults.get("_primary").unwrapped()) .root(); } else { throw new ConfigException.WrongType(onlyKeyValue.origin(), onlyKey, "OBJECT", onlyKeyValue.valueType().toString()); } } ConfigObject fieldValues = (ConfigObject) onlyKeyValue; return fieldValues .withValue(classField, ConfigValueFactory.fromAnyRef(onlyKey, "single key to type from " + root.origin().description())) .withFallback(aliasDefaults); } catch (ClassNotFoundException ignored) { } } // inlined type String matched = null; for (String alias : pluginMap.inlinedAliases()) { if (root.get(alias) != null) { if (matched != null) { String message = String.format( "no type specified, more than one key, and both %s and %s match for inlined types.", matched, alias); throw new ConfigException.Parse(root.origin(), message); } matched = alias; } } if (matched != null) { ConfigObject aliasDefaults = pluginMap.aliasDefaults(matched); ConfigValue inlinedValue = root.get(matched); String primaryField = (String) aliasDefaults.get("_primary").unwrapped(); ConfigObject fieldValues = root.toConfig().withValue(primaryField, inlinedValue).root() .withoutKey(matched).withFallback(aliasDefaults); return fieldValues.withValue(classField, ConfigValueFactory.fromAnyRef(matched, "inlined key to type from " + root.origin().description())); } // default type ConfigValue defaultObject = pluginMap.config().root().get("_default"); if (defaultObject != null) { String defaultName = pluginMap.getLastAlias("_default"); ConfigObject aliasDefaults = pluginMap.aliasDefaults("_default"); return root .withValue(classField, ConfigValueFactory.fromAnyRef(defaultName, pluginMap.category() + " default type : " + defaultObject.origin().description())) .withFallback(aliasDefaults); } } return root; } }