diff --git a/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java b/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java index 6faefd8f3..881cf7207 100644 --- a/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java +++ b/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java @@ -190,6 +190,44 @@ public static String toLowerCaseAndDotted(final String name) { return new String(result, 0, 0, result.length); } + /** + * Matches if a dotted property name is part of a dotted path. + * + * @param path the dotted path + * @param name a dotted property name + * @return true if the dotted property name ir part of a dotted path, or false otherwise. + */ + public static boolean isInPath(final String path, final String name) { + if (name.equals(path)) { + return true; + } + + // if property is less than the root no way to match + if (name.length() <= path.length()) { + return false; + } + + // foo.bar + // foo.bar."baz" + // foo.bar[0] + char e = name.charAt(path.length()); + if ((e == '.') || e == '[') { + for (int i = 0; i < path.length(); i++) { + char r = path.charAt(i); + e = name.charAt(i); + if (r == '-') { + if (e != '.' && e != '-') { + return false; + } + } else if (r != e) { + return false; + } + } + return true; + } + return false; + } + public static boolean isNumeric(final CharSequence digits) { return isNumeric(digits, 0, digits.length()); } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java index 384ed5a36..e3f5b78aa 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java @@ -1,6 +1,8 @@ package io.smallrye.config; +import static io.smallrye.config.ConfigMappingInterface.getNames; import static io.smallrye.config.ConfigValidationException.Problem; +import static io.smallrye.config.ProfileConfigSourceInterceptor.activeName; import static io.smallrye.config.common.utils.StringUtil.unindexed; import static java.util.Collections.EMPTY_MAP; @@ -21,10 +23,12 @@ import java.util.function.IntFunction; import java.util.function.Supplier; +import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.Converter; import io.smallrye.config.ConfigMapping.NamingStrategy; import io.smallrye.config._private.ConfigMessages; +import io.smallrye.config.common.utils.StringUtil; /** * A mapping context. This is used by generated classes during configuration mapping, and is released once the configuration @@ -32,49 +36,55 @@ */ public final class ConfigMappingContext { private final SmallRyeConfig config; + private final ConfigMappingNames names; private final Map, Map> roots = new IdentityHashMap<>(); private final Map, Converter> converterInstances = new IdentityHashMap<>(); private NamingStrategy namingStrategy; - private final StringBuilder stringBuilder = new StringBuilder(); + private String rootPath; + private final StringBuilder nameBuilder = new StringBuilder(); private final Set usedProperties = new HashSet<>(); private final List problems = new ArrayList<>(); - private final ConfigMappingNames names; + public ConfigMappingContext(final SmallRyeConfig config, final Map, Set> roots) { + this(config, new Supplier>>>() { + @Override + public Map>> get() { + // All mapping names must be loaded first because of split mappings + Map>> names = new HashMap<>(); + for (Map.Entry, Set> mapping : roots.entrySet()) { + for (Map.Entry>> entry : ConfigMappingLoader + .configMappingNames(mapping.getKey()).entrySet()) { + names.putIfAbsent(entry.getKey(), new HashMap<>()); + names.get(entry.getKey()).putAll(entry.getValue()); + } + } + return names; + } + }.get(), roots); + } - public ConfigMappingContext( + ConfigMappingContext( final SmallRyeConfig config, - final Map>> roots, - final Map>> names) { + final Map>> names, + final Map, Set> roots) { this.config = config; this.names = new ConfigMappingNames(names); - - for (Map.Entry>> entry : roots.entrySet()) { - String path = entry.getKey(); - for (Class root : entry.getValue()) { - registerRoot(root, path); + matchPropertiesWithEnv(roots); + for (Map.Entry, Set> mapping : roots.entrySet()) { + Map mappingObjects = new HashMap<>(); + for (String rootPath : mapping.getValue()) { + applyNamingStrategy(null); + applyRootPath(rootPath); + applyNameBuilder(rootPath); + mappingObjects.put(rootPath, (ConfigMappingObject) constructRoot(mapping.getKey())); } + this.roots.put(mapping.getKey(), mappingObjects); } } - private void registerRoot(Class rootType, String rootPath) { - roots.computeIfAbsent(rootType, new Function, Map>() { - @Override - public Map apply(final Class mapping) { - return new HashMap<>(); - } - }).computeIfAbsent(rootPath, new Function() { - @Override - public ConfigMappingObject apply(final String path) { - namingStrategy = null; - stringBuilder.replace(0, stringBuilder.length(), rootPath); - return (ConfigMappingObject) constructRoot(rootType); - } - }); - } - - public T constructRoot(Class interfaceType) { + T constructRoot(Class interfaceType) { return constructGroup(interfaceType); } @@ -113,7 +123,7 @@ public Converter getConverterInstance(Class, Map> getRootsMap() { return roots; } + private void matchPropertiesWithEnv(final Map, Set> roots) { + // TODO - We shouldn't be mutating the EnvSource. + // We should do the calculation when creating the EnvSource, but right now mappings and sources are not well integrated. + + Set rootPaths = new HashSet<>(); + for (Set paths : roots.values()) { + rootPaths.addAll(paths); + } + boolean all = rootPaths.contains(""); + StringBuilder sb = new StringBuilder(); + + for (ConfigSource configSource : config.getConfigSources(EnvConfigSource.class)) { + if (roots.isEmpty()) { + break; + } + + EnvConfigSource envConfigSource = (EnvConfigSource) configSource; + Set mutableEnvProperties = envConfigSource.getPropertyNames(); + List envProperties = new ArrayList<>(mutableEnvProperties); + for (String envProperty : envProperties) { + String activeEnvProperty; + if (envProperty.charAt(0) == '%') { + activeEnvProperty = activeName(envProperty, config.getProfiles()); + } else { + activeEnvProperty = envProperty; + } + + String matchedRoot = null; + if (!all) { + for (String rootPath : rootPaths) { + if (StringUtil.isInPath(rootPath, activeEnvProperty)) { + matchedRoot = rootPath; + break; + } + } + if (matchedRoot == null) { + continue; + } + } else { + matchedRoot = ""; + } + + for (Map> mappingsNames : names.getNames().values()) { + Set propertyNames = mappingsNames.get(new PropertyName("")); + if (propertyNames == null) { + continue; + } + + for (PropertyName mappedName : propertyNames) { + String name = matchedRoot.isEmpty() ? mappedName.getName() : matchedRoot + "." + mappedName.getName(); + // Try to match Env with Root mapped property and generate the expected format + List indexOfDashes = indexOfDashes(name, activeEnvProperty); + if (indexOfDashes != null) { + sb.append(activeEnvProperty); + for (Integer dash : indexOfDashes) { + sb.setCharAt(dash, '-'); + } + String expectedEnvProperty = sb.toString(); + if (!activeEnvProperty.equals(expectedEnvProperty)) { + envConfigSource.getPropertyNames().add(sb.toString()); + envConfigSource.getPropertyNames().remove(envProperty); + // TODO - /~https://github.com/quarkusio/quarkus/issues/38479 + //ignoredPaths.add(activeEnvProperty); + } + sb.setLength(0); + break; + } + } + } + } + } + } + + /** + * Finds and returns all indexes from a dotted Environment property name, related to its matched mapped + * property name that must be replaced with a dash. This allows to set single environment variables as + * FOO_BAR_BAZ and match them to mappeds properties like foo.*.baz, + * foo-bar.baz or any other combinations find in mappings, without the need of additional metadata. + * + * @param mappedProperty the mapping property name. + * @param envProperty a generated dotted property from the {@link EnvConfigSource}. + * @return a List of indexes from the env property name to replace with a dash. + */ + private static List indexOfDashes(final String mappedProperty, final String envProperty) { + if (mappedProperty.length() > envProperty.length()) { + return null; + } + + List dashesPosition = null; + int matchPosition = envProperty.length() - 1; + for (int i = mappedProperty.length() - 1; i >= 0; i--) { + if (matchPosition == -1) { + return null; + } + + char c = mappedProperty.charAt(i); + if (c == '.' || c == '-') { + char p = envProperty.charAt(matchPosition); + if (p != '.' && p != '-') { // a property coming from env can either be . or - + return null; + } + if (c == '-') { + if (dashesPosition == null) { + dashesPosition = new ArrayList<>(); + } + dashesPosition.add(matchPosition); + } + matchPosition--; + } else if (c == '*') { // it's a map - skip to next separator + char p = envProperty.charAt(matchPosition); + if (p == '"') { + matchPosition = envProperty.lastIndexOf('"', matchPosition - 1); + if (matchPosition != -1) { + matchPosition = envProperty.lastIndexOf('.', matchPosition); + } + } + matchPosition = envProperty.lastIndexOf('.', matchPosition); + } else if (c == ']') { // it's a collection - skip to next separator + i = i - 2; + matchPosition = envProperty.lastIndexOf('[', matchPosition); + if (matchPosition != -1) { + matchPosition--; + } + } else if (c != envProperty.charAt(matchPosition)) { + return null; + } else { + matchPosition--; + } + } + return dashesPosition; + } + void reportUnknown(final List ignoredPaths) { KeyMap ignoredProperties = new KeyMap<>(); for (String ignoredPath : ignoredPaths) { @@ -287,10 +440,10 @@ public ObjectCreator map( supplier = new Supplier() { @Override public V get() { - int length = stringBuilder.length(); - stringBuilder.append(".*"); + int length = nameBuilder.length(); + nameBuilder.append(".*"); V defaultValue = constructGroup(defaultClass); - stringBuilder.setLength(length); + nameBuilder.setLength(length); return defaultValue; } }; @@ -451,7 +604,7 @@ public ObjectCreator group(final Class groupType) { creator.accept(new Function() { @Override public G apply(final String path) { - StringBuilder sb = ConfigMappingContext.this.getStringBuilder(); + StringBuilder sb = ConfigMappingContext.this.getNameBuilder(); int length = sb.length(); sb.append(path, length, path.length()); G group = constructGroup(groupType); @@ -469,7 +622,7 @@ public ObjectCreator lazyGroup(final Class groupType) { @Override public G apply(final String path) { if (createRequired(groupType, path)) { - StringBuilder sb = ConfigMappingContext.this.getStringBuilder(); + StringBuilder sb = ConfigMappingContext.this.getNameBuilder(); int length = sb.length(); sb.append(path, length, path.length()); G group = constructGroup(groupType); @@ -490,7 +643,7 @@ public ObjectCreator optionalGroup(final Class groupType) { @Override public Optional apply(final String path) { if (createRequired(groupType, path)) { - StringBuilder sb = ConfigMappingContext.this.getStringBuilder(); + StringBuilder sb = ConfigMappingContext.this.getNameBuilder(); int length = sb.length(); sb.append(path, length, path.length()); G group = constructGroup(groupType); @@ -671,17 +824,7 @@ private Converter getConverter(final Class rawType, final Class boolean createRequired(final Class groupType, final String path) { - Set names = ConfigMappingContext.this.names.get(groupType.getName(), path); - if (names == null) { - return false; - } - - for (String propertyName : config.getPropertyNames()) { - if (propertyName.startsWith(path) && names.contains(new PropertyName(propertyName))) { - return true; - } - } - return false; + return ConfigMappingContext.this.names.hasAnyName(groupType.getName(), rootPath, path, config.getPropertyNames()); } private IntFunction> createCollectionFactory(final Class type) { diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java index 071f586f0..8d270f185 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java @@ -5,6 +5,7 @@ import static org.objectweb.asm.Opcodes.ACC_INTERFACE; import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_STATIC; import static org.objectweb.asm.Opcodes.ACONST_NULL; import static org.objectweb.asm.Opcodes.ALOAD; import static org.objectweb.asm.Opcodes.ARETURN; @@ -40,6 +41,7 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.util.HashSet; +import java.util.Map; import java.util.Set; import java.util.regex.Pattern; @@ -83,7 +85,6 @@ public class ConfigMappingGenerator { private static final String I_STRING_BUILDER = getInternalName(StringBuilder.class); private static final String I_STRING = getInternalName(String.class); private static final String I_NAMING_STRATEGY = getInternalName(NamingStrategy.class); - private static final String I_SET = getInternalName(Set.class); private static final String I_FIELD = getInternalName(Field.class); private static final int V_THIS = 0; @@ -129,7 +130,7 @@ static byte[] generate(final ConfigMappingInterface mapping) { // stack: - ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT); // stack: ctxt - ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getStringBuilder", "()L" + I_STRING_BUILDER + ';', + ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getNameBuilder", "()L" + I_STRING_BUILDER + ';', false); // stack: sb ctor.visitInsn(DUP); @@ -158,13 +159,14 @@ static byte[] generate(final ConfigMappingInterface mapping) { "(L" + I_NAMING_STRATEGY + ";)L" + I_NAMING_STRATEGY + ";", false); ctor.visitVarInsn(ASTORE, V_NAMING_STRATEGY); - // stack: - addProperties(visitor, ctor, new HashSet<>(), mapping, mapping.getClassInternalName()); - // stack: - if (mapping.getToStringMethod().generate()) { addToString(visitor, mapping); } - // stack: - + + generateNames(visitor, mapping); + generateDefaults(visitor, mapping); + ctor.visitInsn(RETURN); ctor.visitLabel(ctorEnd); ctor.visitLocalVariable("mc", 'L' + I_MAPPING_CONTEXT + ';', null, ctorStart, ctorEnd, V_MAPPING_CONTEXT); @@ -752,6 +754,81 @@ private static void addToString(final ClassVisitor visitor, final ConfigMappingI ts.visitMaxs(0, 0); } + private static void generateNames(final ClassVisitor classVisitor, final ConfigMappingInterface mapping) { + MethodVisitor mv = classVisitor.visitMethod(ACC_PUBLIC | ACC_STATIC, "getNames", "()Ljava/util/Map;", + "()Ljava/util/Map;>;>;", + null); + + mv.visitTypeInsn(NEW, "java/util/HashMap"); + mv.visitInsn(DUP); + mv.visitMethodInsn(INVOKESPECIAL, "java/util/HashMap", "", "()V", false); + mv.visitVarInsn(ASTORE, 0); + + for (Map.Entry>> mappings : mapping.getNames().entrySet()) { + mv.visitTypeInsn(NEW, "java/util/HashMap"); + mv.visitInsn(DUP); + mv.visitMethodInsn(INVOKESPECIAL, "java/util/HashMap", "", "()V", false); + mv.visitVarInsn(ASTORE, 1); + for (Map.Entry> paths : mappings.getValue().entrySet()) { + mv.visitTypeInsn(NEW, "java/util/HashSet"); + mv.visitInsn(DUP); + mv.visitMethodInsn(INVOKESPECIAL, "java/util/HashSet", "", "()V", false); + mv.visitVarInsn(ASTORE, 2); + for (String name : paths.getValue()) { + mv.visitVarInsn(ALOAD, 2); + mv.visitLdcInsn(name); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Set", "add", "(Ljava/lang/Object;)Z", true); + mv.visitInsn(POP); + } + mv.visitVarInsn(ALOAD, 1); + mv.visitLdcInsn(paths.getKey()); + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", true); + mv.visitInsn(POP); + } + mv.visitVarInsn(ALOAD, 0); + mv.visitLdcInsn(mappings.getKey()); + mv.visitVarInsn(ALOAD, 1); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", true); + mv.visitInsn(POP); + } + mv.visitVarInsn(ALOAD, 0); + mv.visitInsn(ARETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + private static void generateDefaults(final ClassVisitor classVisitor, final ConfigMappingInterface mapping) { + MethodVisitor mv = classVisitor.visitMethod(ACC_PUBLIC | ACC_STATIC, "getDefaults", "()Ljava/util/Map;", + "()Ljava/util/Map;", + null); + + mv.visitTypeInsn(NEW, "java/util/HashMap"); + mv.visitInsn(DUP); + mv.visitMethodInsn(INVOKESPECIAL, "java/util/HashMap", "", "()V", false); + mv.visitVarInsn(ASTORE, 0); + + for (Map.Entry entry : ConfigMappingInterface.getProperties(mapping) + .get(mapping.getInterfaceType()) + .get("").entrySet()) { + if (entry.getValue().hasDefaultValue()) { + mv.visitVarInsn(ALOAD, 0); + mv.visitLdcInsn(entry.getKey()); + mv.visitLdcInsn(entry.getValue().getDefaultValue()); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", true); + mv.visitInsn(POP); + } + } + + mv.visitVarInsn(ALOAD, 0); + mv.visitInsn(ARETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + private static boolean hasDefaultValue(final Class klass, final Object value) { if (value == null) { return false; diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java index 23e65cfbd..cb03098e7 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java @@ -29,7 +29,7 @@ import io.smallrye.config._private.ConfigMessages; /** - * Information about a configuration interface. + * The metadata reprensentation of a {@link ConfigMapping} annotated class. */ public final class ConfigMappingInterface implements ConfigMappingMetadata { static final ConfigMappingInterface[] NO_TYPES = new ConfigMappingInterface[0]; @@ -89,7 +89,7 @@ public Class getInterfaceType() { return interfaceType; } - public ConfigMappingInterface[] getSuperTypes() { + ConfigMappingInterface[] getSuperTypes() { return superTypes; } @@ -99,6 +99,16 @@ public Property[] getProperties() { return properties.toArray(new Property[0]); } + /** + * Constructs a representation of all {@link Property} names contained in the {@link ConfigMappingInterface}. + * + * @return a Map with the mapping properties names + * @see ConfigMappingInterface#getNames(ConfigMappingInterface) + */ + public Map>> getNames() { + return ConfigMappingInterface.getNames(this); + } + private static Set getSuperProperties(ConfigMappingInterface type) { Set properties = new HashSet<>(); for (ConfigMappingInterface superType : type.getSuperTypes()) { @@ -1009,4 +1019,252 @@ static Class rawTypeOf(final Type type) { throw ConfigMessages.msg.noRawType(type); } } + + /** + * Constructs a representation of all {@link Property} names contained in the {@link ConfigMappingInterface}. + *

+ * + *

    + *
  • The first level Map key is each Class name that is part of the mapping
  • + *
  • The second level Map key is each String path
  • + *
  • The Set contains all {@link Property} String names under the mapping path, + * including sub-elements and nested elements
  • + *
+ * + * The path names do not include the {@link ConfigMapping#prefix()} because the same {@link ConfigMappingInterface} + * can be registered for multiple prefixes. + *

+ * The mapping class root {@link Property} use the empty String for the path key. + * + * @param configMapping a {@link ConfigMappingInterface} representation of a {@link ConfigMapping} annotated class + * @return a Map with the mapping properties names + */ + public static Map>> getNames(final ConfigMappingInterface configMapping) { + Map>> names = new HashMap<>(); + Map, Map>> properties = getProperties(configMapping); + for (Map.Entry, Map>> entry : properties.entrySet()) { + Map> groups = new HashMap<>(); + for (Map.Entry> group : entry.getValue().entrySet()) { + groups.put(group.getKey(), group.getValue().keySet()); + } + names.put(entry.getKey().getName(), groups); + } + return names; + } + + /** + * Constructs a representation of all {@link Property} contained in the {@link ConfigMappingInterface}. + *

+ * + *

    + *
  • The first level Map key is each Class that is part of the mapping
  • + *
  • The second level Map key is each String path in the mapping to a {@link Property}
  • + *
  • The third level Map key is each String {@link Property} name under the mapping + * path, including sub-elements and nested elements
  • + *
+ * + * The path names do not include the {@link ConfigMapping#prefix()} because the same {@link ConfigMappingInterface} + * can be registered for multiple prefixes. + *

+ * The mapping class root {@link Property} use the empty String for the path key. + * + * @param configMapping a {@link ConfigMappingInterface} representation of a {@link ConfigMapping} annotated class + * @return a Map with {@link Property} values + */ + public static Map, Map>> getProperties(final ConfigMappingInterface configMapping) { + Map, Map>> properties = new HashMap<>(); + getProperties(new GroupProperty(null, null, configMapping), configMapping.getNamingStrategy(), new Path(), properties); + return properties; + } + + private static void getProperties( + final GroupProperty groupProperty, + final NamingStrategy namingStrategy, + final Path path, + final Map, Map>> properties) { + + ConfigMappingInterface groupType = groupProperty.getGroupType(); + Map groupProperties = properties + .computeIfAbsent(groupType.getInterfaceType(), group -> new HashMap<>()) + .computeIfAbsent(path.get(), s -> new HashMap<>()); + + getProperties(groupProperty, namingStrategy, path, properties, groupProperties); + } + + private static void getProperties( + final GroupProperty groupProperty, + final NamingStrategy namingStrategy, + final Path path, + final Map, Map>> properties, + final Map groupProperties) { + + for (Property property : groupProperty.getGroupType().getProperties()) { + getProperty(property, namingStrategy, path, properties, groupProperties); + } + } + + private static void getProperty( + final Property property, + final NamingStrategy namingStrategy, + final Path path, + final Map, Map>> properties, + final Map groupProperties) { + + if (property.isLeaf() || property.isPrimitive()) { + groupProperties.put(path.get(property, namingStrategy), property); + } else if (property.isGroup()) { + GroupProperty groupProperty = property.asGroup(); + NamingStrategy groupNamingStrategy = groupProperty.hasNamingStrategy() ? groupProperty.getNamingStrategy() + : namingStrategy; + Path groupPath = path.group(groupProperty, namingStrategy); + getProperties(groupProperty, groupNamingStrategy, groupPath, properties); + getProperties(groupProperty, groupNamingStrategy, groupPath, properties, groupProperties); + } else if (property.isMap()) { + ConfigMappingInterface.MapProperty mapProperty = property.asMap(); + if (mapProperty.getValueProperty().isLeaf()) { + groupProperties.put(path.map(property, namingStrategy).get(), property); + if (mapProperty.hasKeyUnnamed()) { + groupProperties.put(path.unnamedMap(property, namingStrategy).get(), property); + } + } else if (mapProperty.getValueProperty().isGroup()) { + GroupProperty groupProperty = mapProperty.getValueProperty().asGroup(); + NamingStrategy groupNamingStrategy = groupProperty.hasNamingStrategy() ? groupProperty.getNamingStrategy() + : namingStrategy; + Path mapPath = path.map(mapProperty, namingStrategy); + getProperties(groupProperty, groupNamingStrategy, mapPath, properties); + getProperties(groupProperty, groupNamingStrategy, mapPath, properties, groupProperties); + if (mapProperty.hasKeyUnnamed()) { + Path unnamedMapPath = path.unnamedMap(mapProperty, namingStrategy); + getProperties(groupProperty, groupNamingStrategy, unnamedMapPath, properties); + getProperties(groupProperty, groupNamingStrategy, unnamedMapPath, properties, groupProperties); + } + } else if (mapProperty.getValueProperty().isCollection()) { + CollectionProperty collectionProperty = mapProperty.getValueProperty().asCollection(); + Property element = collectionProperty.getElement(); + if (element.isLeaf()) { + groupProperties.put(path.map().collection().get(property, namingStrategy), property); + // We use a different leaf to remove the default, to not duplicate defaults properties, because + // leaf collections can use the single name or use the name with square brackets + groupProperties.put(path.map().get(property, namingStrategy), + new LeafProperty(element.getMethod(), element.getPropertyName(), element.asLeaf().getValueType(), + element.asLeaf().getConvertWith(), null)); + if (mapProperty.hasKeyUnnamed()) { + groupProperties.put(path.collection().get(property, namingStrategy), property); + } + } else { + getProperty(element, namingStrategy, path.map().collection(), properties, groupProperties); + if (mapProperty.hasKeyUnnamed()) { + getProperty(element, namingStrategy, path.collection(), properties, groupProperties); + } + } + } else if (mapProperty.getValueProperty().isMap()) { + getProperty(mapProperty.getValueProperty(), namingStrategy, path.map(), properties, groupProperties); + if (mapProperty.hasKeyUnnamed()) { + getProperty(mapProperty.getValueProperty(), namingStrategy, path.unnamedMap(), properties, groupProperties); + } + } + } else if (property.isCollection()) { + CollectionProperty collectionProperty = property.asCollection(); + if (collectionProperty.getElement().isLeaf()) { + getProperty(collectionProperty.getElement(), namingStrategy, path, properties, groupProperties); + } + getProperty(collectionProperty.getElement(), namingStrategy, path.collection(), properties, groupProperties); + } else if (property.isOptional()) { + getProperty(property.asOptional().getNestedProperty(), namingStrategy, path, properties, groupProperties); + } + } + + static class Path { + private final String path; + private final List elements; + + Path() { + this(""); + } + + Path(final String path) { + this(path, new ArrayList<>()); + } + + Path(final String path, final List elements) { + this.path = path; + this.elements = elements; + } + + Path group(final String path) { + return new Path(get(path)); + } + + Path group(final Property property, final NamingStrategy namingStrategy) { + if (property.isParentPropertyName()) { + return group(""); + } else { + return group(property.getPropertyName(namingStrategy)); + } + } + + Path map() { + List elements = new ArrayList<>(this.elements); + elements.add(path.isEmpty() && elements.isEmpty() ? "*" : ".*"); + return new Path(get(), elements); + } + + Path map(final String path) { + return new Path(get(path)); + } + + Path map(final Property property, final NamingStrategy namingStrategy) { + if (property.isParentPropertyName()) { + this.elements.add(path.isEmpty() && this.elements.isEmpty() ? "*" : ".*"); + return map(""); + } else { + this.elements.add(".*"); + return map(property.getPropertyName(namingStrategy)); + } + } + + Path collection() { + List elements = new ArrayList<>(this.elements); + elements.add("[*]"); + return new Path(get(), elements); + } + + Path unnamedMap() { + return this; + } + + Path unnamedMap(final String path) { + return new Path(get(path)); + } + + Path unnamedMap(final Property property, final NamingStrategy namingStrategy) { + return unnamedMap(property.isParentPropertyName() ? "" : property.getPropertyName(namingStrategy)); + } + + String get(final String path) { + String elements = String.join("", this.elements); + this.elements.clear(); + if (this.path.isEmpty()) { + if (path.isEmpty()) { + return elements; + } else { + if (!elements.isEmpty() && elements.charAt(0) == '*') { + return path + "." + elements; + } else { + return path + elements; + } + } + } else { + return path.isEmpty() ? this.path + elements : this.path + "." + path + elements; + } + } + + String get(final Property property, final NamingStrategy namingStrategy) { + return get(property.isParentPropertyName() ? "" : property.getPropertyName(namingStrategy)); + } + + String get() { + return path; + } + } } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingLoader.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingLoader.java index 0ecae4d3e..bb9c85023 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingLoader.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingLoader.java @@ -3,9 +3,12 @@ import java.lang.invoke.MethodHandles; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.UndeclaredThrowableException; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.eclipse.microprofile.config.inject.ConfigProperties; @@ -50,7 +53,7 @@ public static ConfigMappingInterface getConfigMapping(final Class type) { static Class getConfigMappingClass(final Class type) { validateAnnotations(type); - final ConfigMappingClass configMappingClass = ConfigMappingClass.getConfigurationClass(type); + ConfigMappingClass configMappingClass = ConfigMappingClass.getConfigurationClass(type); if (configMappingClass == null) { return type; } else { @@ -58,6 +61,45 @@ static Class getConfigMappingClass(final Class type) { } } + @SuppressWarnings("unchecked") + static Map>> configMappingNames(final Class interfaceType) { + try { + Method getNames = CACHE.get(interfaceType).getImplementationClass().getDeclaredMethod("getNames"); + return (Map>>) getNames.invoke(null); + } catch (NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } catch (IllegalAccessException e) { + throw new IllegalAccessError(e.getMessage()); + } catch (InvocationTargetException e) { + try { + throw e.getCause(); + } catch (RuntimeException | Error e2) { + throw e2; + } catch (Throwable t) { + throw new UndeclaredThrowableException(t); + } + } + } + + static Map configMappingDefaults(final Class interfaceType) { + try { + Method getDefaults = CACHE.get(interfaceType).getImplementationClass().getDeclaredMethod("getDefaults"); + return (Map) getDefaults.invoke(null); + } catch (NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } catch (IllegalAccessException e) { + throw new IllegalAccessError(e.getMessage()); + } catch (InvocationTargetException e) { + try { + throw e.getCause(); + } catch (RuntimeException | Error e2) { + throw e2; + } catch (Throwable t) { + throw new UndeclaredThrowableException(t); + } + } + } + static T configMappingObject(final Class interfaceType, final ConfigMappingContext configMappingContext) { ConfigMappingObject instance; try { diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingNames.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingNames.java index 01e3e1e65..172bfe971 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingNames.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingNames.java @@ -1,51 +1,117 @@ package io.smallrye.config; +import static io.smallrye.config.PropertyName.name; + import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +/** + * Represents the full structure of mapping classes per class name, relative path and property name. + */ class ConfigMappingNames { - private final Map names; + private final Map>> names; - public ConfigMappingNames(final Map>> names) { + ConfigMappingNames(final Map>> names) { this.names = new HashMap<>(names.size()); - for (Map.Entry>> entry : names.entrySet()) { - this.names.put(entry.getKey(), new Names(entry.getValue())); + + for (Map.Entry>> mappings : names.entrySet()) { + Map> mappingPropertyNames = new HashMap<>(); + for (Map.Entry> mappingNames : mappings.getValue().entrySet()) { + PropertyName key = name(mappingNames.getKey()); + mappingPropertyNames.putIfAbsent(key, new HashSet<>()); + // Give priority to the star key + if (key.getName().contains("*")) { + mappingPropertyNames.put(key, mappingPropertyNames.remove(key)); + } + + Set values = mappingPropertyNames.get(key); + for (String value : mappingNames.getValue()) { + // Give priority to the star key + if (value.contains("*")) { + values.remove(new PropertyName(value)); + } + values.add(new PropertyName(value)); + } + mappingPropertyNames.get(key).addAll(values); + } + this.names.put(mappings.getKey(), mappingPropertyNames); } } - Set get(String mapping, String name) { - return names.get(mapping).get(name); + public Map>> getNames() { + return names; } - private static class Names { - private final Map> names; - private final Map> anys; - - Names(Map> names) { - this.names = new HashMap<>(); - this.anys = new HashMap<>(); - for (Map.Entry> entry : names.entrySet()) { - if (entry.getKey().indexOf('*') == -1) { - this.names.put(new PropertyName(entry.getKey()), toMappingNameSet(entry.getValue())); - } else { - this.anys.put(new PropertyName(entry.getKey()), toMappingNameSet(entry.getValue())); + /** + * Matches that at least one runtime configuration name is in the root path and relative path of a mapping class. + * This is required to trigger the construction of lazy mapping objects like Optional or + * Map. + * + * @param mapping the class name of the mapping + * @param rootPath the root path of the mapping + * @param path the relative path to the mapping + * @param names the runtime config names + * @return true if a runtime config name exits in the mapping names or false otherwise + */ + boolean hasAnyName(final String mapping, final String rootPath, final String path, final Iterable names) { + Map> mappings = this.names.get(mapping); + if (mappings == null) { + return false; + } + + // Simple case, no need to remove the rootPath from searched path and names + if (rootPath == null || rootPath.isEmpty()) { + return hasAnyName(mappings, path, names); + } + + // The path does not match the rootPath or the next char is not a separator dot we can skip + if (!path.startsWith(rootPath) || (path.length() > rootPath.length() && path.charAt(rootPath.length()) != '.' + && path.charAt(rootPath.length()) != '[')) { + return false; + } + + // Same length replace with empty string since we know they already match, or start after the next separator + PropertyName mappingName; + if (path.length() == rootPath.length()) { + mappingName = new PropertyName(""); + } else if (path.charAt(rootPath.length()) == '.') { + mappingName = new PropertyName(path.substring(rootPath.length() + 1)); + } else { + mappingName = new PropertyName(path.substring(rootPath.length())); + } + Set mappingNames = mappings.get(mappingName); + if (mappingNames == null) { + return false; + } + + for (String name : names) { + // We can't check for next char separator dot because we may find collection names with square brackets + if (name.startsWith(path)) { + if (mappingNames + .contains(new PropertyName(name.charAt(rootPath.length()) == '.' ? name.substring(rootPath.length() + 1) + : name.substring(rootPath.length())))) { + return true; } } } - Set get(String name) { - PropertyName mappingName = new PropertyName(name); - return names.getOrDefault(mappingName, anys.get(mappingName)); + return false; + } + + boolean hasAnyName(final Map> mappings, final String path, + final Iterable names) { + Set mappingNames = mappings.get(name(path)); + if (mappingNames == null || mappingNames.isEmpty()) { + return false; } - private static Set toMappingNameSet(Set names) { - Set mappingNames = new HashSet<>(names.size()); - for (String name : names) { - mappingNames.add(new PropertyName(name)); + for (String name : names) { + if (name.startsWith(path) && mappingNames.contains(name(name))) { + return true; } - return mappingNames; } + return false; } } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java deleted file mode 100644 index ecaa7fb5e..000000000 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java +++ /dev/null @@ -1,381 +0,0 @@ -package io.smallrye.config; - -import static io.smallrye.config.ConfigMappingLoader.getConfigMappingClass; -import static io.smallrye.config.ConfigMappings.getDefaults; -import static io.smallrye.config.ConfigMappings.getNames; -import static io.smallrye.config.ConfigMappings.getProperties; -import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; -import static io.smallrye.config.ProfileConfigSourceInterceptor.*; -import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN; -import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores; -import static io.smallrye.config.common.utils.StringUtil.toLowerCaseAndDotted; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import org.eclipse.microprofile.config.spi.ConfigSource; - -import io.smallrye.common.constraint.Assert; -import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix; - -/** - * - */ -final class ConfigMappingProvider implements Serializable { - private static final long serialVersionUID = 3977667610888849912L; - - private final Map>> roots; - private final Map>> names; - private final List ignoredPaths; - private final boolean validateUnknown; - - ConfigMappingProvider(final Builder builder) { - this.roots = new HashMap<>(builder.roots); - this.names = builder.names; - this.ignoredPaths = builder.ignoredPaths; - this.validateUnknown = builder.validateUnknown; - } - - public static Builder builder() { - return new Builder(); - } - - Map, Map> mapConfiguration(final SmallRyeConfig config) - throws ConfigValidationException { - // Register additional dissabiguation property names comparing mapped keys and env names - matchPropertiesWithEnv(config); - - if (roots.isEmpty()) { - return Collections.emptyMap(); - } - - // Perform the config mapping - ConfigMappingContext context = SecretKeys.doUnlocked(new Supplier() { - @Override - public ConfigMappingContext get() { - return new ConfigMappingContext(config, roots, names); - } - }); - - if (config.getOptionalValue(SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN, boolean.class).orElse(this.validateUnknown)) { - context.reportUnknown(ignoredPaths); - } - - List problems = context.getProblems(); - if (!problems.isEmpty()) { - throw new ConfigValidationException(problems.toArray(ConfigValidationException.Problem.NO_PROBLEMS)); - } - - return context.getRootsMap(); - } - - private void matchPropertiesWithEnv(final SmallRyeConfig config) { - // TODO - We shouldn't be mutating the EnvSource. - // We should do the calculation when creating the EnvSource, but right now mappings and sources are not well integrated. - - List profiles = config.getProfiles(); - boolean all = roots.containsKey(""); - StringBuilder sb = new StringBuilder(); - - for (ConfigSource configSource : config.getConfigSources(EnvConfigSource.class)) { - if (roots.isEmpty()) { - break; - } - - EnvConfigSource envConfigSource = (EnvConfigSource) configSource; - Set mutableEnvProperties = envConfigSource.getPropertyNames(); - List envProperties = new ArrayList<>(mutableEnvProperties); - for (String envProperty : envProperties) { - String activeEnvProperty; - if (envProperty.charAt(0) == '%') { - activeEnvProperty = activeName(envProperty, profiles); - } else { - activeEnvProperty = envProperty; - } - - String matchedRoot = null; - if (!all) { - for (String root : roots.keySet()) { - if (isEnvPropertyInRoot(activeEnvProperty, root)) { - matchedRoot = root; - break; - } - } - if (matchedRoot == null) { - continue; - } - } else { - matchedRoot = ""; - } - - for (Map> rootNames : names.values()) { - Set mappedProperties = rootNames.get(matchedRoot); - if (mappedProperties != null) { - for (String mappedProperty : mappedProperties) { - // Try to match Env with Root mapped property and generate the expected format - List indexOfDashes = indexOfDashes(mappedProperty, activeEnvProperty); - if (indexOfDashes != null) { - sb.append(activeEnvProperty); - for (Integer dash : indexOfDashes) { - sb.setCharAt(dash, '-'); - } - String expectedEnvProperty = sb.toString(); - if (!activeEnvProperty.equals(expectedEnvProperty)) { - envConfigSource.getPropertyNames().add(sb.toString()); - envConfigSource.getPropertyNames().remove(envProperty); - ignoredPaths.add(activeEnvProperty); - } - sb.setLength(0); - break; - } - } - break; - } - } - } - } - - // Match dotted properties from other sources with Env with the same semantic meaning - // This needs to happen after matching dashed names from mappings - List dottedProperties = new ArrayList<>(); - for (ConfigSource configSource : config.getConfigSources()) { - if (!(configSource instanceof EnvConfigSource)) { - Set propertyNames = configSource.getPropertyNames(); - if (propertyNames != null) { - dottedProperties.addAll(propertyNames.stream().map(new Function() { - @Override - public String apply(final String name) { - return activeName(name, profiles); - } - }).collect(Collectors.toList())); - } - } - } - - for (ConfigSource configSource : config.getConfigSources(EnvConfigSource.class)) { - EnvConfigSource envConfigSource = (EnvConfigSource) configSource; - for (String dottedProperty : dottedProperties) { - Set envNames = envConfigSource.getPropertyNames(); - if (envConfigSource.hasPropertyName(dottedProperty)) { - if (!envNames.contains(dottedProperty)) { - // this may be expensive, but it shouldn't happend that often - envNames.remove(toLowerCaseAndDotted(replaceNonAlphanumericByUnderscores(dottedProperty))); - envNames.add(dottedProperty); - } - } - } - } - } - - /** - * Matches if a dotted Environment property name is part of a registered root. - * - * @param envProperty a generated dotted property from the {@link EnvConfigSource}. - * @param root the root name - * @return true if the env property ir part of the root, or false otherwise. - */ - private static boolean isEnvPropertyInRoot(final String envProperty, final String root) { - if (envProperty.equals(root)) { - return true; - } - - // if property is less than the root no way to match - if (envProperty.length() <= root.length()) { - return false; - } - - // foo.bar - // foo.bar."baz" - // foo.bar[0] - char e = envProperty.charAt(root.length()); - if ((e == '.') || e == '[') { - for (int i = 0; i < root.length(); i++) { - char r = root.charAt(i); - e = envProperty.charAt(i); - if (r == '-') { - if (e != '.' && e != '-') { - return false; - } - } else if (r != e) { - return false; - } - } - return true; - } - return false; - } - - /** - * Finds and returns all indexes from a dotted Environment property name, related to its matched mapped - * property name that must be replaced with a dash. This allows to set single environment variables as - * FOO_BAR_BAZ and match them to mappeds properties like foo.*.baz, - * foo-bar.baz or any other combinations find in mappings, without the need of additional metadata. - * - * @param mappedProperty the mapping property name. - * @param envProperty a generated dotted property from the {@link EnvConfigSource}. - * @return a List of indexes from the env property name to replace with a dash. - */ - private static List indexOfDashes(final String mappedProperty, final String envProperty) { - if (mappedProperty.length() > envProperty.length()) { - return null; - } - - List dashesPosition = null; - int matchPosition = envProperty.length() - 1; - for (int i = mappedProperty.length() - 1; i >= 0; i--) { - if (matchPosition == -1) { - return null; - } - - char c = mappedProperty.charAt(i); - if (c == '.' || c == '-') { - char p = envProperty.charAt(matchPosition); - if (p != '.' && p != '-') { // a property coming from env can either be . or - - return null; - } - if (c == '-') { - if (dashesPosition == null) { - dashesPosition = new ArrayList<>(); - } - dashesPosition.add(matchPosition); - } - matchPosition--; - } else if (c == '*') { // it's a map - skip to next separator - char p = envProperty.charAt(matchPosition); - if (p == '"') { - matchPosition = envProperty.lastIndexOf('"', matchPosition - 1); - if (matchPosition != -1) { - matchPosition = envProperty.lastIndexOf('.', matchPosition); - } - } - matchPosition = envProperty.lastIndexOf('.', matchPosition); - } else if (c == ']') { // it's a collection - skip to next separator - i = i - 2; - matchPosition = envProperty.lastIndexOf('[', matchPosition); - if (matchPosition != -1) { - matchPosition--; - } - } else if (c != envProperty.charAt(matchPosition)) { - return null; - } else { - matchPosition--; - } - } - return dashesPosition; - } - - static final class Builder { - Set> types = new HashSet<>(); - Map>> roots = new HashMap<>(); - Set keys = new HashSet<>(); - Map>> names = new HashMap<>(); - List ignoredPaths = new ArrayList<>(); - boolean validateUnknown = true; - SmallRyeConfigBuilder configBuilder = null; - - Builder() { - } - - Builder addRoot(String prefix, Class type) { - Assert.checkNotNullParam("path", prefix); - Assert.checkNotNullParam("type", type); - types.add(type); - roots.computeIfAbsent(prefix, k -> new ArrayList<>(4)).add(getConfigMappingClass(type)); - return this; - } - - Builder keys(Set keys) { - Assert.checkNotNullParam("keys", keys); - this.keys.addAll(keys); - return this; - } - - Builder names(Map>> names) { - Assert.checkNotNullParam("names", names); - for (Map.Entry>> entry : names.entrySet()) { - Map> groupNames = this.names.computeIfAbsent(entry.getKey(), - new Function>>() { - @Override - public Map> apply( - final String s) { - return new HashMap<>(); - } - }); - groupNames.putAll(entry.getValue()); - } - return this; - } - - Builder ignoredPath(String ignoredPath) { - Assert.checkNotNullParam("ignoredPath", ignoredPath); - ignoredPaths.add(ignoredPath); - return this; - } - - Builder validateUnknown(boolean validateUnknown) { - this.validateUnknown = validateUnknown; - return this; - } - - Builder registerDefaults(SmallRyeConfigBuilder configBuilder) { - this.configBuilder = configBuilder; - return this; - } - - ConfigMappingProvider build() { - // We don't validate for MP ConfigProperties, so if all classes are MP ConfigProperties disable validation. - boolean allConfigurationProperties = true; - for (Class type : types) { - if (ConfigMappingClass.getConfigurationClass(type) == null) { - allConfigurationProperties = false; - break; - } - } - - if (allConfigurationProperties) { - validateUnknown = false; - } - - if (keys.isEmpty()) { - for (Map.Entry>> entry : roots.entrySet()) { - for (Class root : entry.getValue()) { - ConfigClassWithPrefix configClass = configClassWithPrefix(root, entry.getKey()); - keys(getProperties(configClass).get(configClass.getKlass()).get(configClass.getPrefix()).keySet()); - } - } - } - - if (names.isEmpty()) { - for (Map.Entry>> entry : roots.entrySet()) { - for (Class root : entry.getValue()) { - ConfigClassWithPrefix configClass = configClassWithPrefix(root, entry.getKey()); - names(getNames(configClass)); - } - } - } - - if (configBuilder != null) { - Map defaultValues = configBuilder.getDefaultValues(); - for (Map.Entry>> entry : roots.entrySet()) { - for (Class root : entry.getValue()) { - for (Map.Entry defaultEntry : getDefaults(configClassWithPrefix(root, entry.getKey())) - .entrySet()) { - defaultValues.putIfAbsent(defaultEntry.getKey(), defaultEntry.getValue()); - } - } - } - } - - return new ConfigMappingProvider(this); - } - } -} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappings.java b/implementation/src/main/java/io/smallrye/config/ConfigMappings.java index d7b77039d..a08d4ac68 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappings.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappings.java @@ -1,257 +1,113 @@ package io.smallrye.config; -import static io.smallrye.config.ConfigMappingLoader.getConfigMappingClass; import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; -import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN; -import static java.lang.Boolean.TRUE; -import java.io.Serializable; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Function; -import io.smallrye.config.ConfigMapping.NamingStrategy; -import io.smallrye.config.ConfigMappingInterface.CollectionProperty; -import io.smallrye.config.ConfigMappingInterface.GroupProperty; -import io.smallrye.config.ConfigMappingInterface.LeafProperty; import io.smallrye.config.ConfigMappingInterface.Property; -public final class ConfigMappings implements Serializable { - private static final long serialVersionUID = -7790784345796818526L; - +/** + * Utility class for {@link ConfigMapping} annotated classes. + */ +public final class ConfigMappings { + + /** + * Registers additional {@link ConfigMapping} annotated classes with a {@link SmallRyeConfig} instance. + *

+ * The recommended method of registering {@link ConfigMapping} is with a + * {@link SmallRyeConfigBuilder#withMapping(Class, String)}. In certain cases, this is not possible (ex. a CDI + * runtime), where mapping classes can only be discovered after the Config instance creation. + * + * @param config the {@link SmallRyeConfig} instance + * @param configClasses a Set of {@link ConfigMapping} annotated classes with prefixes + * @throws ConfigValidationException if a {@link ConfigMapping} cannot be registed with the + * {@link SmallRyeConfig} instance + */ public static void registerConfigMappings(final SmallRyeConfig config, final Set configClasses) throws ConfigValidationException { if (!configClasses.isEmpty()) { - Boolean validateUnknown = config.getOptionalValue(SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN, Boolean.class) - .orElse(TRUE); - mapConfiguration(config, ConfigMappingProvider.builder().validateUnknown(validateUnknown), configClasses); + mapConfiguration(config, new SmallRyeConfigBuilder(), configClasses); } } + /** + * Registers additional ConfigProperties> annotated classes with a {@link SmallRyeConfig} instance. + *

+ * The recommended method of registering ConfigProperties> is with a + * {@link SmallRyeConfigBuilder#withMapping(Class, String)}. In certain cases, this is not possible (ex. a CDI + * runtime), where mapping classes can only be discovered after the Config instance creation. + * + * @param config the {@link SmallRyeConfig} instance + * @param configClasses a Set of ConfigProperties> annotated classes with prefixes + * @throws ConfigValidationException if a ConfigProperties> cannot be registed with the + * {@link SmallRyeConfig} instance + */ public static void registerConfigProperties(final SmallRyeConfig config, final Set configClasses) throws ConfigValidationException { if (!configClasses.isEmpty()) { - mapConfiguration(config, ConfigMappingProvider.builder().validateUnknown(false), configClasses); + mapConfiguration(config, new SmallRyeConfigBuilder().withValidateUnknown(false), configClasses); } } - public static Map, Map>> getProperties(final ConfigClassWithPrefix configClass) { - Map, Map>> properties = new HashMap<>(); - Function path = new Function<>() { - @Override - public String apply(final String path) { - return configClass.getPrefix().isEmpty() && !path.isEmpty() ? path.substring(1) - : configClass.getPrefix() + path; - } - }; - ConfigMappingInterface configMapping = ConfigMappingLoader.getConfigMapping(configClass.getKlass()); - getProperties(new GroupProperty(null, null, configMapping), configMapping.getNamingStrategy(), path, properties); - return properties; - } - - public static Map>> getNames(final ConfigClassWithPrefix configClass) { - Map>> names = new HashMap<>(); - Map, Map>> properties = getProperties(configClass); - for (Map.Entry, Map>> entry : properties.entrySet()) { - Map> groups = new HashMap<>(); - for (Map.Entry> group : entry.getValue().entrySet()) { - groups.put(group.getKey(), group.getValue().keySet()); - } - names.put(entry.getKey().getName(), groups); + /** + * Constructs a representation of all {@link Property} contained in a mapping class. The Map key is + * the full path to the {@link Property}, including the mapping class prefix. + * + * @param configClass the {@link ConfigMapping} annotated class and String prefix + * @see ConfigMappingInterface#getProperties(ConfigMappingInterface) + * @return a Map with all mapping class {@link Property}. + */ + public static Map getProperties(final ConfigClassWithPrefix configClass) { + Map properties = new HashMap<>(); + // Because the properties key names do not include the path prefix we need to add it + for (Map.Entry entry : ConfigMappingInterface + .getProperties(ConfigMappingLoader.getConfigMapping(configClass.getKlass())) + .get(configClass.getKlass()) + .get("").entrySet()) { + properties.put(prefix(configClass.getPrefix(), entry.getKey()), entry.getValue()); } - return names; - } - - public static Set getKeys(final ConfigClassWithPrefix configClass) { - return getProperties(configClass).get(configClass.getKlass()).get(configClass.getPrefix()).keySet(); - } - - public static Map getDefaults(final ConfigClassWithPrefix configClass) { - Map defaults = new HashMap<>(); - Map, Map>> properties = getProperties(configClass); - for (Map.Entry entry : properties.get(configClass.getKlass()).get(configClass.getPrefix()) - .entrySet()) { - if (entry.getValue().hasDefaultValue()) { - defaults.put(entry.getKey(), entry.getValue().getDefaultValue()); - } - } - return defaults; + return properties; } + @Deprecated public static Set mappedProperties(final ConfigClassWithPrefix configClass, final Set properties) { - Set names = new ConfigMappingNames(getNames(configClass)) - .get(configClass.getKlass().getName(), configClass.getPrefix()); - Set mappedProperties = new HashSet<>(); + ConfigMappingNames names = new ConfigMappingNames( + ConfigMappingLoader.getConfigMapping(configClass.getKlass()).getNames()); + Set mappedNames = new HashSet<>(); for (String property : properties) { - if (names.contains(new PropertyName(property))) { - mappedProperties.add(property); + if (names.hasAnyName(configClass.getKlass().getName(), configClass.getPrefix(), configClass.getPrefix(), + Set.of(property))) { + mappedNames.add(property); } } - return mappedProperties; + return mappedNames; } private static void mapConfiguration( final SmallRyeConfig config, - final ConfigMappingProvider.Builder builder, + final SmallRyeConfigBuilder configBuilder, final Set configClasses) throws ConfigValidationException { - DefaultValuesConfigSource defaultValues = (DefaultValuesConfigSource) config.getDefaultValues(); for (ConfigClassWithPrefix configClass : configClasses) { - builder.addRoot(configClass.getPrefix(), configClass.getKlass()); - defaultValues.addDefaults( - getDefaults(configClassWithPrefix(getConfigMappingClass(configClass.getKlass()), configClass.getPrefix()))); + configBuilder.withMapping(configClass.getKlass(), configClass.getPrefix()); } - config.getMappings().putAll(builder.build().mapConfiguration(config)); + config.getDefaultValues().addDefaults(configBuilder.getDefaultValues()); + config.getMappings().putAll(config.buildMappings(configBuilder)); } - private static void getProperties( - final GroupProperty groupProperty, - final NamingStrategy namingStrategy, - final Function path, - final Map, Map>> properties) { - - ConfigMappingInterface groupType = groupProperty.getGroupType(); - Map groupProperties = properties - .computeIfAbsent(groupType.getInterfaceType(), group -> new HashMap<>()) - .computeIfAbsent(path.apply(""), s -> new HashMap<>()); - - getProperties(groupProperty, namingStrategy, path, properties, groupProperties); - } - - private static void getProperties( - final GroupProperty groupProperty, - final NamingStrategy namingStrategy, - final Function path, - final Map, Map>> properties, - final Map groupProperties) { - - for (Property property : groupProperty.getGroupType().getProperties()) { - getProperty(property, namingStrategy, path, properties, groupProperties); - } - } - - private static void getProperty( - final Property property, - final NamingStrategy namingStrategy, - final Function path, - final Map, Map>> properties, - final Map groupProperties) { - - if (property.isLeaf()) { - groupProperties.put( - path.apply(property.isParentPropertyName() ? "" : "." + property.getPropertyName(namingStrategy)), - property); - } else if (property.isPrimitive()) { - groupProperties.put( - path.apply(property.isParentPropertyName() ? "" : "." + property.getPropertyName(namingStrategy)), - property); - } else if (property.isGroup()) { - GroupProperty groupProperty = property.asGroup(); - NamingStrategy groupNamingStrategy = groupProperty.hasNamingStrategy() ? groupProperty.getNamingStrategy() - : namingStrategy; - Function groupPath = new Function<>() { - @Override - public String apply(final String name) { - return property.isParentPropertyName() ? path.apply("") + name - : path.apply("." + property.getPropertyName(namingStrategy)) + name; - } - }; - getProperties(groupProperty, groupNamingStrategy, groupPath, properties); - getProperties(groupProperty, groupNamingStrategy, groupPath, properties, groupProperties); - } else if (property.isMap()) { - ConfigMappingInterface.MapProperty mapProperty = property.asMap(); - if (mapProperty.getValueProperty().isLeaf()) { - groupProperties.put(property.isParentPropertyName() ? path.apply(".*") - : path.apply("." + property.getPropertyName(namingStrategy) + ".*"), mapProperty); - if (mapProperty.hasKeyUnnamed()) { - groupProperties.put(property.isParentPropertyName() ? path.apply("") - : path.apply("." + property.getPropertyName(namingStrategy)), mapProperty); - } - } else if (mapProperty.getValueProperty().isGroup()) { - GroupProperty groupProperty = mapProperty.getValueProperty().asGroup(); - NamingStrategy groupNamingStrategy = groupProperty.hasNamingStrategy() ? groupProperty.getNamingStrategy() - : namingStrategy; - Function groupPath = new Function<>() { - @Override - public String apply(final String name) { - return property.isParentPropertyName() ? path.apply(".*") + name - : path.apply("." + mapProperty.getPropertyName(namingStrategy) + ".*") + name; - } - }; - getProperties(groupProperty, groupNamingStrategy, groupPath, properties); - getProperties(groupProperty, groupNamingStrategy, groupPath, properties, groupProperties); - if (mapProperty.hasKeyUnnamed()) { - Function unnamedGroupPath = new Function<>() { - @Override - public String apply(final String name) { - return property.isParentPropertyName() ? path.apply(name) - : path.apply("." + mapProperty.getPropertyName(namingStrategy)) + name; - } - }; - getProperties(groupProperty, groupNamingStrategy, unnamedGroupPath, properties); - getProperties(groupProperty, groupNamingStrategy, unnamedGroupPath, properties, groupProperties); - } - } else if (mapProperty.getValueProperty().isCollection()) { - CollectionProperty collectionProperty = mapProperty.getValueProperty().asCollection(); - Property element = collectionProperty.getElement(); - if (element.isLeaf()) { - LeafProperty leafProperty = new LeafProperty(element.getMethod(), element.getPropertyName(), - element.asLeaf().getValueType(), element.asLeaf().getConvertWith(), null); - getProperty(leafProperty, namingStrategy, new Function() { - @Override - public String apply(final String name) { - return path.apply(name + ".*"); - } - }, properties, groupProperties); - } - getProperty(element, namingStrategy, new Function() { - @Override - public String apply(final String name) { - return path.apply(name + ".*[*]"); - } - }, properties, groupProperties); - if (mapProperty.hasKeyUnnamed()) { - getProperty(element, namingStrategy, new Function() { - @Override - public String apply(final String name) { - return path.apply(name + "[*]"); - } - }, properties, groupProperties); - } - } else if (mapProperty.getValueProperty().isMap()) { - getProperty(mapProperty.getValueProperty(), namingStrategy, - new Function() { - @Override - public String apply(final String name) { - return path.apply(name + ".*"); - } - }, properties, groupProperties); - if (mapProperty.hasKeyUnnamed()) { - getProperty(mapProperty.getValueProperty(), namingStrategy, - new Function() { - @Override - public String apply(final String name) { - return path.apply(name); - } - }, properties, groupProperties); - } - } - } else if (property.isCollection()) { - CollectionProperty collectionProperty = property.asCollection(); - if (collectionProperty.getElement().isLeaf()) { - getProperty(collectionProperty.getElement(), namingStrategy, path, properties, groupProperties); - } - getProperty(collectionProperty.getElement(), namingStrategy, new Function() { - @Override - public String apply(final String name) { - return path.apply(name.endsWith(".*") ? name.substring(0, name.length() - 2) + "[*].*" : name + "[*]"); - } - }, properties, groupProperties); - } else if (property.isOptional()) { - getProperty(property.asOptional().getNestedProperty(), namingStrategy, path, properties, groupProperties); + static String prefix(final String prefix, final String path) { + if (prefix.isEmpty()) { + return path; + } else if (path.isEmpty()) { + return prefix; + } else if (path.charAt(0) == '[') { + return prefix + path; + } else { + return prefix + "." + path; } } @@ -260,6 +116,10 @@ static String getPrefix(Class type) { return configMapping != null ? configMapping.prefix() : ""; } + /** + * A representation of a {@link ConfigMapping} or @ConfigProperties with a Class and the + * prefix. + */ public static final class ConfigClassWithPrefix { private final Class klass; private final String prefix; diff --git a/implementation/src/main/java/io/smallrye/config/DefaultValuesConfigSource.java b/implementation/src/main/java/io/smallrye/config/DefaultValuesConfigSource.java index b9c14cdf0..c946105c7 100644 --- a/implementation/src/main/java/io/smallrye/config/DefaultValuesConfigSource.java +++ b/implementation/src/main/java/io/smallrye/config/DefaultValuesConfigSource.java @@ -43,7 +43,7 @@ void addDefault(final String name, final String value) { if (name.indexOf('*') == -1) { this.properties.putIfAbsent(name, value); } else { - this.wildcards.put(new PropertyName(name), value); + this.wildcards.putIfAbsent(new PropertyName(name), value); } } } diff --git a/implementation/src/main/java/io/smallrye/config/PropertyName.java b/implementation/src/main/java/io/smallrye/config/PropertyName.java index 9067ffce6..f51a3d5df 100644 --- a/implementation/src/main/java/io/smallrye/config/PropertyName.java +++ b/implementation/src/main/java/io/smallrye/config/PropertyName.java @@ -54,11 +54,7 @@ static boolean equals(final String name, final String other) { } else if (o == '"') { int beginQuote = other.lastIndexOf('"', i - 1); if (beginQuote != -1) { - if (beginQuote != 0 && other.charAt(beginQuote - 1) == '.') { - i = beginQuote; - } else { - i = beginQuote; - } + i = beginQuote; } } else { int previousDot = other.lastIndexOf('.', i); diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java index a71e01d31..52b6774cc 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java @@ -20,6 +20,9 @@ import static io.smallrye.config.Converters.newCollectionConverter; import static io.smallrye.config.Converters.newMapConverter; import static io.smallrye.config.Converters.newOptionalConverter; +import static io.smallrye.config.ProfileConfigSourceInterceptor.activeName; +import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores; +import static io.smallrye.config.common.utils.StringUtil.toLowerCaseAndDotted; import static io.smallrye.config.common.utils.StringUtil.unindexed; import static io.smallrye.config.common.utils.StringUtil.unquoted; import static java.util.stream.Collectors.toList; @@ -40,7 +43,10 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import java.util.function.IntFunction; +import java.util.function.Supplier; +import java.util.stream.Collectors; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -76,12 +82,14 @@ public class SmallRyeConfig implements Config, Serializable { private final Map, Map> mappings; SmallRyeConfig(SmallRyeConfigBuilder builder) { - // This needs to be executed before everything else to make sure that defaults from mappings are available to all sources - ConfigMappingProvider mappingProvider = builder.getMappingsBuilder().build(); this.configSources = new ConfigSources(builder, this); this.converters = buildConverters(builder); this.configValidator = builder.getValidator(); - this.mappings = new ConcurrentHashMap<>(mappingProvider.mapConfiguration(this)); + this.mappings = new ConcurrentHashMap<>(buildMappings(builder)); + + // Match dotted properties from other sources with Env with the same semantic meaning + // This needs to happen after matching dashed names from mappings + matchPropertiesWithEnv(); } private Map> buildConverters(final SmallRyeConfigBuilder builder) { @@ -106,6 +114,64 @@ private Map> buildConverters(final SmallRyeConfigBuilder buil return converters; } + Map, Map> buildMappings(final SmallRyeConfigBuilder builder) + throws ConfigValidationException { + SmallRyeConfigBuilder.MappingBuilder mappingsBuilder = builder.getMappingsBuilder(); + if (mappingsBuilder.getMappings().isEmpty()) { + return Collections.emptyMap(); + } + + // Perform the config mapping + ConfigMappingContext context = SecretKeys.doUnlocked(new Supplier() { + @Override + public ConfigMappingContext get() { + return new ConfigMappingContext(SmallRyeConfig.this, mappingsBuilder.getMappings()); + } + }); + + if (getOptionalValue(SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN, boolean.class).orElse(true)) { + context.reportUnknown(mappingsBuilder.getIgnoredPaths()); + } + + List problems = context.getProblems(); + if (!problems.isEmpty()) { + throw new ConfigValidationException(problems.toArray(ConfigValidationException.Problem.NO_PROBLEMS)); + } + + return context.getRootsMap(); + } + + private void matchPropertiesWithEnv() { + List dottedProperties = new ArrayList<>(); + for (ConfigSource configSource : getConfigSources()) { + if (!(configSource instanceof EnvConfigSource)) { + Set propertyNames = configSource.getPropertyNames(); + if (propertyNames != null) { + dottedProperties.addAll(propertyNames.stream().map(new Function() { + @Override + public String apply(final String name) { + return activeName(name, getProfiles()); + } + }).collect(Collectors.toList())); + } + } + } + + for (ConfigSource configSource : getConfigSources(EnvConfigSource.class)) { + EnvConfigSource envConfigSource = (EnvConfigSource) configSource; + for (String dottedProperty : dottedProperties) { + Set envNames = envConfigSource.getPropertyNames(); + if (envConfigSource.hasPropertyName(dottedProperty)) { + if (!envNames.contains(dottedProperty)) { + // this may be expensive, but it shouldn't happen that often + envNames.remove(toLowerCaseAndDotted(replaceNonAlphanumericByUnderscores(dottedProperty))); + envNames.add(dottedProperty); + } + } + } + } + } + @Override public List getValues(final String name, final Class propertyType) { return getValues(name, propertyType, ArrayList::new); @@ -609,6 +675,10 @@ public Optional getConfigSource(final String name) { return Optional.empty(); } + DefaultValuesConfigSource getDefaultValues() { + return configSources.defaultValues; + } + public T convert(String value, Class asType) { return value != null ? requireConverter(asType).convert(value) : null; } @@ -677,10 +747,6 @@ public List getProfiles() { return configSources.getProfiles(); } - public ConfigSource getDefaultValues() { - return configSources.defaultValues; - } - ConfigSourceInterceptorContext interceptorChain() { return configSources.interceptorChain; } @@ -690,7 +756,7 @@ private static class ConfigSources implements Serializable { private final List profiles; private final List sources; - private final ConfigSource defaultValues; + private final DefaultValuesConfigSource defaultValues; private final ConfigSourceInterceptorContext interceptorChain; private final PropertyNames propertyNames; diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java index df5732f5e..d51b57294 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java @@ -16,6 +16,8 @@ package io.smallrye.config; +import static io.smallrye.config.ConfigMappingLoader.getConfigMappingClass; +import static io.smallrye.config.ConfigMappings.prefix; import static io.smallrye.config.ConfigSourceInterceptorFactory.DEFAULT_PRIORITY; import static io.smallrye.config.Converters.STRING_CONVERTER; import static io.smallrye.config.Converters.newCollectionConverter; @@ -52,6 +54,7 @@ import org.eclipse.microprofile.config.spi.ConfigSourceProvider; import org.eclipse.microprofile.config.spi.Converter; +import io.smallrye.common.constraint.Assert; import io.smallrye.config._private.ConfigMessages; /** @@ -71,7 +74,7 @@ public class SmallRyeConfigBuilder implements ConfigBuilder { private final List secretKeysHandlers = new ArrayList<>(); private ConfigValidator validator = ConfigValidator.EMPTY; private final Map defaultValues = new HashMap<>(); - private final ConfigMappingProvider.Builder mappingsBuilder = ConfigMappingProvider.builder(); + private final MappingBuilder mappingsBuilder = new MappingBuilder(); private ClassLoader classLoader = SecuritySupport.getContextClassLoader(); private boolean addDiscoveredCustomizers = false; private boolean addDefaultSources = false; @@ -82,10 +85,6 @@ public class SmallRyeConfigBuilder implements ConfigBuilder { private boolean addDiscoveredSecretKeysHandlers = false; private boolean addDiscoveredValidator = false; - public SmallRyeConfigBuilder() { - withMappingDefaults(true); - } - public SmallRyeConfigBuilder addDiscoveredCustomizers() { addDiscoveredCustomizers = true; return this; @@ -177,10 +176,6 @@ ConfigValidator discoverValidator() { return ConfigValidator.EMPTY; } - ConfigMappingProvider.Builder getMappingsBuilder() { - return mappingsBuilder; - } - @Override public SmallRyeConfigBuilder addDefaultSources() { addDefaultSources = true; @@ -515,7 +510,7 @@ public SmallRyeConfigBuilder withMapping(Class klass) { } public SmallRyeConfigBuilder withMapping(Class klass, String prefix) { - mappingsBuilder.addRoot(prefix, klass); + mappingsBuilder.mapping(klass, prefix); return this; } @@ -525,26 +520,10 @@ public SmallRyeConfigBuilder withMappingIgnore(String path) { } public SmallRyeConfigBuilder withValidateUnknown(boolean validateUnknown) { - mappingsBuilder.validateUnknown(validateUnknown); withDefaultValue(SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN, Boolean.toString(validateUnknown)); return this; } - public SmallRyeConfigBuilder withMappingDefaults(boolean mappingDefaults) { - mappingsBuilder.registerDefaults(mappingDefaults ? this : null); - return this; - } - - public SmallRyeConfigBuilder withMappingNames(final Map>> names) { - mappingsBuilder.names(names); - return this; - } - - public SmallRyeConfigBuilder withMappingKeys(final Set keys) { - mappingsBuilder.keys(keys); - return this; - } - public SmallRyeConfigBuilder withValidator(ConfigValidator validator) { this.validator = validator; return this; @@ -621,6 +600,10 @@ public Map getDefaultValues() { return defaultValues; } + public MappingBuilder getMappingsBuilder() { + return mappingsBuilder; + } + public ClassLoader getClassLoader() { return classLoader; } @@ -708,6 +691,43 @@ public SmallRyeConfig build() { return new SmallRyeConfig(this); } + final class MappingBuilder { + private final Map, Set> mappings = new HashMap<>(); + private final List ignoredPaths = new ArrayList<>(); + + MappingBuilder mapping(Class type, String prefix) { + Assert.checkNotNullParam("type", type); + Assert.checkNotNullParam("path", prefix); + Class mappingClass = getConfigMappingClass(type); + // It is an MP ConfigProperties, so ignore unmapped properties + if (ConfigMappingClass.getConfigurationClass(type) != null) { + ignoredPaths.add(prefix.isEmpty() ? "*" : prefix + ".**"); + } + mappings.computeIfAbsent(mappingClass, k -> new HashSet<>(4)).add(prefix); + // Load the mapping defaults, to make the defaults available to all config sources + + for (Map.Entry defaultEntry : ConfigMappingLoader.configMappingDefaults(mappingClass).entrySet()) { + // Do not override builder defaults with mapping defaults + defaultValues.putIfAbsent(prefix(prefix, defaultEntry.getKey()), defaultEntry.getValue()); + } + return this; + } + + MappingBuilder ignoredPath(String ignoredPath) { + Assert.checkNotNullParam("ignoredPath", ignoredPath); + ignoredPaths.add(ignoredPath); + return this; + } + + Map, Set> getMappings() { + return mappings; + } + + List getIgnoredPaths() { + return ignoredPaths; + } + } + static class ConverterWithPriority { private final Converter converter; private final int priority; diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java index 8e0c13dde..72853e926 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java @@ -935,4 +935,38 @@ public String convert(final String value) throws IllegalArgumentException, NullP } } } + + @Test + void withParentList() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "list[0].value", "value", + "list[0].optional.value", "value", + "list[1].value", "value")) + .withMapping(WithParentList.class) + .build(); + + WithParentList mapping = config.getConfigMapping(WithParentList.class); + + assertEquals("value", mapping.list().get(0).value()); + assertTrue(mapping.list().get(0).optional().isPresent()); + assertEquals("value", mapping.list().get(0).optional().get().value()); + assertEquals("value", mapping.list().get(1).value()); + } + + @ConfigMapping(prefix = "list") + interface WithParentList { + @WithParentName + List list(); + + interface Nested { + String value(); + + Optional optional(); + + interface NestedOptional { + String value(); + } + } + } } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingDefaultsTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingDefaultsTest.java index bd5396503..be5118bf6 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingDefaultsTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingDefaultsTest.java @@ -2,8 +2,6 @@ import static io.smallrye.config.ConfigMappingDefaultsTest.S3BuildTimeConfig.AsyncHttpClientBuildTimeConfig.AsyncClientType.NETTY; import static io.smallrye.config.ConfigMappingDefaultsTest.S3BuildTimeConfig.SyncHttpClientBuildTimeConfig.SyncClientType.URL; -import static io.smallrye.config.ConfigMappings.getDefaults; -import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; import static io.smallrye.config.KeyValuesConfigSource.config; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -17,6 +15,7 @@ import java.util.OptionalInt; import java.util.Set; +import org.eclipse.microprofile.config.spi.ConfigSource; import org.junit.jupiter.api.Test; import io.smallrye.config.ConfigMappingDefaultsTest.DataSourcesJdbcBuildTimeConfig.DataSourceJdbcOuterNamedBuildTimeConfig; @@ -555,10 +554,14 @@ interface DataSourceJdbcTracingRuntimeConfig { @Test void parentDefaults() { - Map defaults = getDefaults(configClassWithPrefix(ExtendsBase.class)); - assertEquals(2, defaults.size()); - assertEquals("default", defaults.get("base.base")); - assertEquals("default", defaults.get("base.my-prop")); + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(ExtendsBase.class) + .build(); + + ConfigSource defaults = config.getDefaultValues(); + assertEquals(2, defaults.getPropertyNames().size()); + assertEquals("default", defaults.getValue("base.base")); + assertEquals("default", defaults.getValue("base.my-prop")); } public interface Base { @@ -676,13 +679,6 @@ public interface DevServicesBuildTimeConfig { @Test void multipleLevelDefaults() { - Map defaults = getDefaults(configClassWithPrefix(GrandChild.class)); - assertEquals("parent", defaults.get("parent")); - assertEquals("child", defaults.get("child")); - assertEquals("grand-child", defaults.get("grand-child")); - assertEquals("child", defaults.get("override-by-child")); - assertEquals("grand-child", defaults.get("override-by-grand-child")); - SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(GrandChild.class) .build(); diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingFullTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingFullTest.java index c10010921..f377bdf37 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingFullTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingFullTest.java @@ -1,6 +1,7 @@ package io.smallrye.config; import static io.smallrye.config.KeyValuesConfigSource.config; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Map; @@ -234,4 +235,65 @@ interface DataSourceJdbcBuildTimeConfig { } } } + + @Test + void splitMappings() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(new EnvConfigSource(Map.of("SMALLRYE_CONFIG_ORM_DATABASE_GENERATION", "drop-and-create"), 100)) + .withMapping(OrmConfig.class) + .withMapping(OrmRuntimeConfig.class) + .build(); + + OrmConfig ormConfig = config.getConfigMapping(OrmConfig.class); + assertEquals(1, ormConfig.persistenceUnits().size()); + OrmRuntimeConfig ormRuntimeConfig = config.getConfigMapping(OrmRuntimeConfig.class); + assertEquals(1, ormRuntimeConfig.persistenceUnits().size()); + assertEquals("drop-and-create", + ormRuntimeConfig.persistenceUnits().get("").database().generation().generation()); + } + + @ConfigMapping(prefix = "smallrye.config-orm") + interface OrmConfig { + OrmConfigDatabase database(); + + @WithParentName + @WithUnnamedKey("") + @WithDefaults + Map persistenceUnits(); + + interface OrmConfigDatabase { + + } + + interface OrmConfigPersistenceUnit { + OrmConfigPersistenceUnitDatabase database(); + + interface OrmConfigPersistenceUnitDatabase { + @WithDefault("false") + boolean globallyQuotedIdentifiers(); + } + } + } + + @ConfigMapping(prefix = "smallrye.config-orm") + interface OrmRuntimeConfig { + @WithParentName + @WithUnnamedKey("") + @WithDefaults + Map persistenceUnits(); + + interface OrmRuntimeConfigPersistenceUnit { + OrmConfigPersistenceUnitDatabase database(); + + interface OrmConfigPersistenceUnitDatabase { + OrmConfigPersistenceUnitDatabaseGeneration generation(); + } + + interface OrmConfigPersistenceUnitDatabaseGeneration { + @WithParentName + @WithDefault("none") + String generation(); + } + } + } } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingsTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingsTest.java index 0aa2978f3..6d18b5686 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingsTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingsTest.java @@ -319,9 +319,11 @@ interface Nested { @Test void properties() { ConfigMappings.ConfigClassWithPrefix configClass = configClassWithPrefix(MappedProperties.class); - Map properties = ConfigMappings.getProperties(configClass).get(configClass.getKlass()) - .get(configClass.getPrefix()); + Map properties = ConfigMappings.getProperties(configClass); assertEquals(3, properties.size()); + assertTrue(properties.containsKey("mapped.nested.value")); + assertTrue(properties.containsKey("mapped.value")); + assertTrue(properties.containsKey("mapped.collection[*].value")); } @Test diff --git a/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java b/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java index 9c13c5848..04cd356f8 100644 --- a/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java +++ b/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java @@ -1,7 +1,5 @@ package io.smallrye.config; -import static io.smallrye.config.ConfigMappings.getNames; -import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; import static io.smallrye.config.KeyValuesConfigSource.config; import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -43,8 +41,8 @@ void objectCreator() { "optional-list-group[0].value", "value")) .build(); - ConfigMappingContext context = new ConfigMappingContext(config, new HashMap<>(), - getNames(configClassWithPrefix(ObjectCreator.class))); + ConfigMappingContext context = new ConfigMappingContext(config, + ConfigMappingLoader.getConfigMapping(ObjectCreator.class).getNames(), new HashMap<>()); ObjectCreator mapping = new ObjectCreatorImpl(context); assertEquals(2, mapping.unnamed().size()); @@ -124,7 +122,7 @@ static class ObjectCreatorImpl implements ObjectCreator { @SuppressWarnings("unchecked") public ObjectCreatorImpl(ConfigMappingContext context) { - StringBuilder sb = context.getStringBuilder(); + StringBuilder sb = context.getNameBuilder(); int length = sb.length(); ConfigMapping.NamingStrategy ns = ConfigMapping.NamingStrategy.KEBAB_CASE; @@ -271,8 +269,8 @@ void optionalGroup() { "optional.value", "value")) .build(); - ConfigMappingContext context = new ConfigMappingContext(config, new HashMap<>(), - getNames(configClassWithPrefix(OptionalGroup.class))); + ConfigMappingContext context = new ConfigMappingContext(config, + ConfigMappingLoader.getConfigMapping(OptionalGroup.class).getNames(), new HashMap<>()); OptionalGroup mapping = new OptionalGroupImpl(context); assertTrue(mapping.optional().isPresent()); @@ -297,7 +295,7 @@ static class OptionalGroupImpl implements OptionalGroup { Optional empty; public OptionalGroupImpl(ConfigMappingContext context) { - StringBuilder sb = context.getStringBuilder(); + StringBuilder sb = context.getNameBuilder(); int length = sb.length(); ConfigMapping.NamingStrategy ns = ConfigMapping.NamingStrategy.KEBAB_CASE; @@ -334,9 +332,10 @@ void unnamedKeys() { "unnamed.key.value", "value")) .build(); - ConfigMappingContext context = new ConfigMappingContext(config, new HashMap<>(), - getNames(configClassWithPrefix(UnnamedKeys.class))); - context.getStringBuilder().append("unnamed"); + ConfigMappingContext context = new ConfigMappingContext(config, + ConfigMappingLoader.getConfigMapping(UnnamedKeys.class).getNames(), new HashMap<>()); + context.applyRootPath("unnamed"); + context.getNameBuilder().append("unnamed"); UnnamedKeys mapping = new UnnamedKeysImpl(context); assertEquals("unnamed", mapping.map().get(null).value()); @@ -364,7 +363,7 @@ static class UnnamedKeysImpl implements UnnamedKeys { Map map; public UnnamedKeysImpl(ConfigMappingContext context) { - StringBuilder sb = context.getStringBuilder(); + StringBuilder sb = context.getNameBuilder(); int length = sb.length(); ConfigMappingContext.ObjectCreator> map = context.new ObjectCreator>( @@ -393,9 +392,10 @@ void mapDefaults() { "map.defaults-list.one[0].value", "value")) .build(); - ConfigMappingContext context = new ConfigMappingContext(config, new HashMap<>(), - getNames(configClassWithPrefix(MapDefaults.class))); - context.getStringBuilder().append("map."); + ConfigMappingContext context = new ConfigMappingContext(config, + ConfigMappingLoader.getConfigMapping(MapDefaults.class).getNames(), new HashMap<>()); + context.applyRootPath("map"); + context.getNameBuilder().append("map."); MapDefaults mapping = new MapDefaultsImpl(context); assertEquals("value", mapping.defaults().get("one")); @@ -442,7 +442,7 @@ static class MapDefaultsImpl implements MapDefaults { Map> defaultsList; public MapDefaultsImpl(ConfigMappingContext context) { - StringBuilder sb = context.getStringBuilder(); + StringBuilder sb = context.getNameBuilder(); int length = sb.length(); ConfigMapping.NamingStrategy ns = ConfigMapping.NamingStrategy.KEBAB_CASE; @@ -502,9 +502,9 @@ void namingStrategy() { "naming.nested_value.value", "value")) .build(); - ConfigMappingContext context = new ConfigMappingContext(config, new HashMap<>(), - getNames(configClassWithPrefix(Naming.class))); - context.getStringBuilder().append("naming."); + ConfigMappingContext context = new ConfigMappingContext(config, + ConfigMappingLoader.getConfigMapping(Naming.class).getNames(), new HashMap<>()); + context.getNameBuilder().append("naming."); Naming naming = new NamingImpl(context); assertEquals("value", naming.nestedValue().value()); @@ -526,7 +526,7 @@ static class NamingImpl implements Naming { public NamingImpl(ConfigMappingContext context) { ConfigMapping.NamingStrategy ns = ConfigMapping.NamingStrategy.SNAKE_CASE; - StringBuilder sb = context.getStringBuilder(); + StringBuilder sb = context.getNameBuilder(); int length = sb.length(); sb.append(ns.apply("nestedValue"));