diff --git a/implementation/pom.xml b/implementation/pom.xml index 154ddf3ce..a77e7180b 100644 --- a/implementation/pom.xml +++ b/implementation/pom.xml @@ -61,6 +61,10 @@ io.smallrye.common smallrye-common-constraint + + io.smallrye.common + smallrye-common-classloader + org.jboss.logging @@ -75,6 +79,11 @@ jboss-logging-processor + + org.ow2.asm + asm + + org.junit.jupiter junit-jupiter diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMapping.java b/implementation/src/main/java/io/smallrye/config/ConfigMapping.java new file mode 100644 index 000000000..a3a55c1bb --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigMapping.java @@ -0,0 +1,14 @@ +package io.smallrye.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ConfigMapping { + String value(); +} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java new file mode 100644 index 000000000..d2484805a --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java @@ -0,0 +1,223 @@ +package io.smallrye.config; + +import static io.smallrye.config.ConfigMappingInterface.LeafProperty; +import static io.smallrye.config.ConfigMappingInterface.MapProperty; +import static io.smallrye.config.ConfigMappingInterface.PrimitiveProperty; +import static io.smallrye.config.ConfigMappingInterface.Property; +import static io.smallrye.config.ConfigMappingInterface.getConfigurationInterface; +import static io.smallrye.config.ConfigMappingInterface.rawTypeOf; +import static io.smallrye.config.ConfigMappingInterface.typeOfParameter; +import static io.smallrye.config.ConfigValidationException.Problem; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.eclipse.microprofile.config.spi.Converter; + +/** + * A mapping context. This is used by generated classes during configuration mapping, and is released once the configuration + * mapping has completed. + */ +public final class ConfigMappingContext { + + private final Map, Map>> enclosedThings = new IdentityHashMap<>(); + private final Map, Map> roots = new IdentityHashMap<>(); + private final Map, Map>> convertersByTypeAndField = new IdentityHashMap<>(); + private final List, Map>>> keyConvertersByDegreeTypeAndField = new ArrayList<>(); + private final Map, Converter> converterInstances = new IdentityHashMap<>(); + private final List allInstances = new ArrayList<>(); + private final SmallRyeConfig config; + private final StringBuilder stringBuilder = new StringBuilder(); + private final ArrayList problems = new ArrayList<>(); + + ConfigMappingContext(final SmallRyeConfig config) { + this.config = config; + } + + public ConfigMappingObject getRoot(Class rootType, String rootPath) { + return roots.getOrDefault(rootType, Collections.emptyMap()).get(rootPath); + } + + public void registerRoot(Class rootType, String rootPath, ConfigMappingObject root) { + roots.computeIfAbsent(rootType, x -> new HashMap<>()).put(rootPath, root); + } + + public Object getEnclosedField(Class enclosingType, String key, Object enclosingObject) { + return enclosedThings + .getOrDefault(enclosingType, Collections.emptyMap()) + .getOrDefault(key, Collections.emptyMap()) + .get(enclosingObject); + } + + public void registerEnclosedField(Class enclosingType, String key, Object enclosingObject, Object value) { + enclosedThings + .computeIfAbsent(enclosingType, x -> new HashMap<>()) + .computeIfAbsent(key, x -> new IdentityHashMap<>()) + .put(enclosingObject, value); + } + + public T constructGroup(Class interfaceType) { + Constructor constructor = getConfigurationInterface(interfaceType).getConstructor(); + ConfigMappingObject instance; + try { + instance = constructor.newInstance(this); + } catch (InstantiationException e) { + throw new InstantiationError(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); + } + } + allInstances.add(instance); + return interfaceType.cast(instance); + } + + @SuppressWarnings({ "unchecked", "unused" }) + public Converter getValueConverter(Class enclosingType, String field) { + return (Converter) convertersByTypeAndField + .computeIfAbsent(enclosingType, x -> new HashMap<>()) + .computeIfAbsent(field, x -> { + ConfigMappingInterface ci = getConfigurationInterface(enclosingType); + Property property = ci.getProperty(field); + boolean optional = property.isOptional(); + if (property.isLeaf() || optional && property.asOptional().getNestedProperty().isLeaf()) { + LeafProperty leafProperty = optional ? property.asOptional().getNestedProperty().asLeaf() + : property.asLeaf(); + if (leafProperty.hasConvertWith()) { + Class> convertWith = leafProperty.getConvertWith(); + // todo: generics + return getConverterInstance(convertWith); + } else { + // todo: replace with generic converter lookup + Class valueRawType = leafProperty.getValueRawType(); + if (valueRawType == List.class) { + return Converters.newCollectionConverter( + config.getConverter(rawTypeOf(typeOfParameter(leafProperty.getValueType(), 0))), + ArrayList::new); + } else if (valueRawType == Set.class) { + return Converters.newCollectionConverter( + config.getConverter(rawTypeOf(typeOfParameter(leafProperty.getValueType(), 0))), + HashSet::new); + } else { + return config.getConverter(valueRawType); + } + } + } else if (property.isPrimitive()) { + PrimitiveProperty primitiveProperty = property.asPrimitive(); + if (primitiveProperty.hasConvertWith()) { + return getConverterInstance(primitiveProperty.getConvertWith()); + } else { + return config.getConverter(primitiveProperty.getBoxType()); + } + } else { + throw new IllegalStateException(); + } + }); + } + + @SuppressWarnings("unchecked") + public Converter getKeyConverter(Class enclosingType, String field, int degree) { + List, Map>>> list = this.keyConvertersByDegreeTypeAndField; + while (list.size() <= degree) { + list.add(new IdentityHashMap<>()); + } + Map, Map>> map = list.get(degree); + return (Converter) map + .computeIfAbsent(enclosingType, x -> new HashMap<>()) + .computeIfAbsent(field, x -> { + ConfigMappingInterface ci = getConfigurationInterface(enclosingType); + MapProperty property = ci.getProperty(field).asMap(); + while (degree + 1 > property.getLevels()) { + property = property.getValueProperty().asMap(); + } + if (property.hasKeyConvertWith()) { + return getConverterInstance(property.getKeyConvertWith()); + } else { + // todo: replace with generic converter lookup + Class valueRawType = property.getKeyRawType(); + if (valueRawType == List.class) { + return Converters.newCollectionConverter( + config.getConverter(rawTypeOf(typeOfParameter(property.getKeyType(), 0))), ArrayList::new); + } else if (valueRawType == Set.class) { + return Converters.newCollectionConverter( + config.getConverter(rawTypeOf(typeOfParameter(property.getKeyType(), 0))), HashSet::new); + } else { + return config.getConverter(valueRawType); + } + } + }); + } + + @SuppressWarnings("unchecked") + public Converter getConverterInstance(Class> converterType) { + return (Converter) converterInstances.computeIfAbsent(converterType, t -> { + try { + return (Converter) t.getConstructor().newInstance(); + } catch (InstantiationException e) { + throw new InstantiationError(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 t2) { + throw new UndeclaredThrowableException(t2); + } + } catch (NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } + }); + } + + public NoSuchElementException noSuchElement(Class type) { + return new NoSuchElementException("A required configuration group of type " + type.getName() + " was not provided"); + } + + public void unknownConfigElement(final String propertyName) { + problems.add(new Problem(propertyName + " does not map to any root")); + } + + void fillInOptionals() { + for (ConfigMappingObject instance : allInstances) { + instance.fillInOptionals(this); + } + } + + public SmallRyeConfig getConfig() { + return config; + } + + public StringBuilder getStringBuilder() { + return stringBuilder; + } + + public void reportProblem(RuntimeException problem) { + problems.add(new Problem(problem.toString())); + } + + ArrayList getProblems() { + return problems; + } + + Map, Map> getRootsMap() { + return roots; + } +} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java new file mode 100644 index 000000000..41f21e448 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java @@ -0,0 +1,1193 @@ +package io.smallrye.config; + +import static io.smallrye.config.ConfigMappingProvider.skewer; +import static org.objectweb.asm.Type.getDescriptor; +import static org.objectweb.asm.Type.getInternalName; +import static org.objectweb.asm.Type.getType; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.microprofile.config.spi.Converter; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import io.smallrye.common.classloader.ClassDefiner; +import io.smallrye.common.constraint.Assert; +import io.smallrye.config.inject.InjectionMessages; + +/** + * Information about a configuration interface. + */ +final class ConfigMappingInterface { + static final ConfigMappingInterface[] NO_TYPES = new ConfigMappingInterface[0]; + static final Property[] NO_PROPERTIES = new Property[0]; + static final ClassValue cv = new ClassValue() { + protected ConfigMappingInterface computeValue(final Class type) { + return createConfigurationInterface(type); + } + }; + static final boolean usefulDebugInfo; + + static { + usefulDebugInfo = Boolean.parseBoolean(AccessController.doPrivileged( + (PrivilegedAction) () -> System.getProperty("io.smallrye.config.mapper.useful-debug-info"))); + } + + private final Class interfaceType; + private final ConfigMappingInterface[] superTypes; + private final Property[] properties; + private final Constructor constructor; + private final Map propertiesByName; + + ConfigMappingInterface(final Class interfaceType, final ConfigMappingInterface[] superTypes, + final Property[] properties) { + this.interfaceType = interfaceType; + this.superTypes = superTypes; + this.properties = properties; + try { + constructor = createConfigurationObjectClass().asSubclass(ConfigMappingObject.class) + .getDeclaredConstructor(ConfigMappingContext.class); + } catch (NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } + final Map propertiesByName = new HashMap<>(); + for (Property property : properties) { + propertiesByName.put(property.getMethod().getName(), property); + } + this.propertiesByName = propertiesByName; + } + + /** + * Get the configuration interface information for the given interface class. This information is cached. + * + * @param interfaceType the interface type (must not be {@code null}) + * @return the configuration interface, or {@code null} if the type does not appear to be a configuration interface + */ + public static ConfigMappingInterface getConfigurationInterface(Class interfaceType) { + Assert.checkNotNullParam("interfaceType", interfaceType); + return cv.get(interfaceType); + } + + /** + * Get the configuration interface type. + * + * @return the configuration interface type + */ + public Class getInterfaceType() { + return interfaceType; + } + + /** + * Get the number of supertypes which define configuration properties. Implemented interfaces which do not + * define any configuration properties and whose supertypes in turn do not define any configuration properties + * are not counted. + * + * @return the number of supertypes + */ + public int getSuperTypeCount() { + return superTypes.length; + } + + /** + * Get the supertype at the given index, which must be greater than or equal to zero and less than the value returned + * by {@link #getSuperTypeCount()}. + * + * @param index the index + * @return the supertype definition + * @throws IndexOutOfBoundsException if {@code index} is invalid + */ + public ConfigMappingInterface getSuperType(int index) throws IndexOutOfBoundsException { + if (index < 0 || index >= superTypes.length) + throw new IndexOutOfBoundsException(); + return superTypes[index]; + } + + /** + * Get the number of properties defined on this type (excluding supertypes). + * + * @return the number of properties + */ + public int getPropertyCount() { + return properties.length; + } + + /** + * Get the property definition at the given index, which must be greater than or equal to zero and less than the + * value returned by {@link #getPropertyCount()}. + * + * @param index the index + * @return the property definition + * @throws IndexOutOfBoundsException if {@code index} is invalid + */ + public Property getProperty(int index) throws IndexOutOfBoundsException { + if (index < 0 || index >= properties.length) + throw new IndexOutOfBoundsException(); + return properties[index]; + } + + public Property getProperty(final String name) { + return propertiesByName.get(name); + } + + Constructor getConstructor() { + return constructor; + } + + public static abstract class Property { + private final Method method; + private final String propertyName; + + Property(final Method method, final String propertyName) { + this.method = method; + this.propertyName = propertyName; + } + + public Method getMethod() { + return method; + } + + public String getPropertyName() { + return Assert.checkNotEmptyParam("propertyName", Assert.checkNotNullParam("propertyName", propertyName)); + } + + public boolean hasPropertyName() { + return propertyName != null; + } + + public boolean isParentPropertyName() { + return hasPropertyName() && propertyName.isEmpty(); + } + + public boolean isPrimitive() { + return false; + } + + public boolean isOptional() { + return false; + } + + public boolean isGroup() { + return false; + } + + public boolean isLeaf() { + return false; + } + + public boolean isMap() { + return false; + } + + public boolean isMayBeOptional() { + return false; + } + + public PrimitiveProperty asPrimitive() { + throw new ClassCastException(); + } + + public OptionalProperty asOptional() { + throw new ClassCastException(); + } + + public GroupProperty asGroup() { + throw new ClassCastException(); + } + + public LeafProperty asLeaf() { + throw new ClassCastException(); + } + + public MapProperty asMap() { + throw new ClassCastException(); + } + + public MayBeOptionalProperty asMayBeOptional() { + throw new ClassCastException(); + } + } + + public static abstract class MayBeOptionalProperty extends Property { + MayBeOptionalProperty(final Method method, final String propertyName) { + super(method, propertyName); + } + + @Override + public boolean isMayBeOptional() { + return true; + } + + @Override + public MayBeOptionalProperty asMayBeOptional() { + return this; + } + } + + public static final class PrimitiveProperty extends Property { + private static final Map, Class> boxTypes; + private static final Map, String> unboxMethodName; + private static final Map, String> unboxMethodDesc; + + static { + Map, Class> map = new HashMap<>(); + map.put(byte.class, Byte.class); + map.put(short.class, Short.class); + map.put(int.class, Integer.class); + map.put(long.class, Long.class); + + map.put(float.class, Float.class); + map.put(double.class, Double.class); + + map.put(boolean.class, Boolean.class); + + map.put(char.class, Character.class); + boxTypes = map; + Map, String> nameMap = new HashMap<>(); + nameMap.put(byte.class, "byteValue"); + nameMap.put(short.class, "shortValue"); + nameMap.put(int.class, "intValue"); + nameMap.put(long.class, "longValue"); + + nameMap.put(float.class, "floatValue"); + nameMap.put(double.class, "doubleValue"); + + nameMap.put(boolean.class, "booleanValue"); + + nameMap.put(char.class, "charValue"); + unboxMethodName = nameMap; + nameMap = new HashMap<>(); + nameMap.put(byte.class, "()B"); + nameMap.put(short.class, "()S"); + nameMap.put(int.class, "()I"); + nameMap.put(long.class, "()J"); + + nameMap.put(float.class, "()F"); + nameMap.put(double.class, "()D"); + + nameMap.put(boolean.class, "()Z"); + + nameMap.put(char.class, "()C"); + unboxMethodDesc = nameMap; + nameMap = new HashMap<>(); + nameMap.put(byte.class, "B"); + nameMap.put(short.class, "S"); + nameMap.put(int.class, "I"); + nameMap.put(long.class, "J"); + + nameMap.put(float.class, "F"); + nameMap.put(double.class, "D"); + + nameMap.put(boolean.class, "Z"); + + nameMap.put(char.class, "C"); + } + + private final Class primitiveType; + private final Class> convertWith; + private final String defaultValue; + + PrimitiveProperty(final Method method, final String propertyName, final Class primitiveType, + final Class> convertWith, final String defaultValue) { + super(method, propertyName); + this.primitiveType = primitiveType; + this.convertWith = convertWith; + this.defaultValue = defaultValue; + } + + public Class getPrimitiveType() { + return primitiveType; + } + + public Class getBoxType() { + return boxTypes.get(primitiveType); + } + + public Class> getConvertWith() { + return Assert.checkNotNullParam("convertWith", convertWith); + } + + public boolean hasConvertWith() { + return convertWith != null; + } + + public String getDefaultValue() { + return Assert.checkNotNullParam("defaultValue", defaultValue); + } + + public boolean hasDefaultValue() { + return defaultValue != null; + } + + @Override + public boolean isPrimitive() { + return true; + } + + @Override + public PrimitiveProperty asPrimitive() { + return this; + } + + String getUnboxMethodName() { + return unboxMethodName.get(primitiveType); + } + + String getUnboxMethodDescriptor() { + return unboxMethodDesc.get(primitiveType); + } + + int getReturnInstruction() { + if (primitiveType == float.class) { + return Opcodes.FRETURN; + } else if (primitiveType == double.class) { + return Opcodes.DRETURN; + } else if (primitiveType == long.class) { + return Opcodes.LRETURN; + } else { + return Opcodes.IRETURN; + } + } + } + + public static final class OptionalProperty extends Property { + private final MayBeOptionalProperty nestedProperty; + + OptionalProperty(final Method method, final String propertyName, final MayBeOptionalProperty nestedProperty) { + super(method, propertyName); + this.nestedProperty = nestedProperty; + } + + @Override + public boolean isOptional() { + return true; + } + + @Override + public OptionalProperty asOptional() { + return this; + } + + @Override + public boolean isLeaf() { + return nestedProperty.isLeaf(); + } + + public MayBeOptionalProperty getNestedProperty() { + return nestedProperty; + } + } + + public static final class GroupProperty extends MayBeOptionalProperty { + private final ConfigMappingInterface groupType; + + GroupProperty(final Method method, final String propertyName, final ConfigMappingInterface groupType) { + super(method, propertyName); + this.groupType = groupType; + } + + public ConfigMappingInterface getGroupType() { + return groupType; + } + + @Override + public boolean isGroup() { + return true; + } + + @Override + public GroupProperty asGroup() { + return this; + } + } + + public static final class LeafProperty extends MayBeOptionalProperty { + private final Type valueType; + private final Class> convertWith; + private final Class rawType; + private final String defaultValue; + + LeafProperty(final Method method, final String propertyName, final Type valueType, + final Class> convertWith, final String defaultValue) { + super(method, propertyName); + this.valueType = valueType; + this.convertWith = convertWith; + rawType = rawTypeOf(valueType); + this.defaultValue = defaultValue; + } + + public Type getValueType() { + return valueType; + } + + public Class> getConvertWith() { + return convertWith; + } + + public boolean hasConvertWith() { + return convertWith != null; + } + + public String getDefaultValue() { + return Assert.checkNotNullParam("defaultValue", defaultValue); + } + + public boolean hasDefaultValue() { + return defaultValue != null; + } + + public Class getValueRawType() { + return rawType; + } + + @Override + public boolean isLeaf() { + return true; + } + + @Override + public LeafProperty asLeaf() { + return this; + } + } + + public static final class MapProperty extends Property { + private final Type keyType; + private final Class> keyConvertWith; + private final Property valueProperty; + + MapProperty(final Method method, final String propertyName, final Type keyType, + final Class> keyConvertWith, final Property valueProperty) { + super(method, propertyName); + this.keyType = keyType; + this.keyConvertWith = keyConvertWith; + this.valueProperty = valueProperty; + } + + public Type getKeyType() { + return keyType; + } + + public Class getKeyRawType() { + return rawTypeOf(keyType); + } + + public Class> getKeyConvertWith() { + return Assert.checkNotNullParam("keyConvertWith", keyConvertWith); + } + + public boolean hasKeyConvertWith() { + return keyConvertWith != null; + } + + public Property getValueProperty() { + return valueProperty; + } + + @Override + public boolean isMap() { + return true; + } + + @Override + public MapProperty asMap() { + return this; + } + + public int getLevels() { + if (valueProperty.isMap()) { + return valueProperty.asMap().getLevels() + 1; + } else { + return 1; + } + } + } + + static ConfigMappingInterface createConfigurationInterface(Class interfaceType) { + if (!interfaceType.isInterface() || interfaceType.getTypeParameters().length != 0) { + return null; + } + // first, find any supertypes + ConfigMappingInterface[] superTypes = getSuperTypes(interfaceType.getInterfaces(), 0, 0); + // now find any properties + Property[] properties = getProperties(interfaceType.getDeclaredMethods(), 0, 0); + // is it anything? + if (superTypes.length == 0 && properties.length == 0) { + // no + return null; + } else { + // it is a proper configuration interface + return new ConfigMappingInterface(interfaceType, superTypes, properties); + } + } + + private static final String I_CLASS = getInternalName(Class.class); + private static final String I_COLLECTIONS = getInternalName(Collections.class); + private static final String I_CONFIGURATION_OBJECT = getInternalName(ConfigMappingObject.class); + private static final String I_CONVERTER = getInternalName(Converter.class); + private static final String I_MAP = getInternalName(Map.class); + private static final String I_MAPPING_CONTEXT = getInternalName(ConfigMappingContext.class); + private static final String I_OBJECT = getInternalName(Object.class); + private static final String I_OPTIONAL = getInternalName(Optional.class); + private static final String I_RUNTIME_EXCEPTION = getInternalName(RuntimeException.class); + private static final String I_SMALLRYE_CONFIG = getInternalName(SmallRyeConfig.class); + private static final String I_STRING_BUILDER = getInternalName(StringBuilder.class); + private static final String I_STRING = getInternalName(String.class); + + private static final int V_THIS = 0; + private static final int V_MAPPING_CONTEXT = 1; + private static final int V_STRING_BUILDER = 2; + private static final int V_LENGTH = 3; + + private void addProperties(ClassVisitor cv, final String className, MethodVisitor ctor, MethodVisitor fio, + Set visited) { + for (Property property : properties) { + Method method = property.getMethod(); + String memberName = method.getName(); + if (!visited.add(memberName)) { + // duplicated property + continue; + } + // the field + String fieldType = getInternalName(method.getReturnType()); + String fieldDesc = getDescriptor(method.getReturnType()); + cv.visitField(Opcodes.ACC_PRIVATE, memberName, fieldDesc, null, null); + + // now process the property + final Property realProperty; + final boolean optional = property.isOptional(); + if (optional) { + realProperty = property.asOptional().getNestedProperty(); + } else { + realProperty = property; + } + + // now handle each possible type + if (property.isMap()) { + // stack: - + ctor.visitMethodInsn(Opcodes.INVOKESTATIC, I_COLLECTIONS, "emptyMap", "()L" + I_MAP + ';', false); + // stack: map + ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: map this + ctor.visitInsn(Opcodes.SWAP); + // stack: this map + ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); + // stack: - + // then sweep it up + // stack: - + fio.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + // stack: ctxt + fio.visitLdcInsn(getType(interfaceType)); + // stack: ctxt iface + fio.visitLdcInsn(memberName); + // stack: ctxt iface name + fio.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: ctxt iface name this + fio.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getEnclosedField", + "(L" + I_CLASS + ";L" + I_STRING + ";L" + I_OBJECT + ";)L" + I_OBJECT + ';', false); + // stack: obj? + fio.visitInsn(Opcodes.DUP); + Label _continue = new Label(); + Label _done = new Label(); + // stack: obj? obj? + fio.visitJumpInsn(Opcodes.IFNULL, _continue); + // stack: obj + fio.visitTypeInsn(Opcodes.CHECKCAST, I_MAP); + // stack: map + fio.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: map this + fio.visitInsn(Opcodes.SWAP); + // stack: this map + fio.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); + // stack: - + fio.visitJumpInsn(Opcodes.GOTO, _done); + fio.visitLabel(_continue); + // stack: null + fio.visitInsn(Opcodes.POP); + // stack: - + fio.visitLabel(_done); + } else if (property.isGroup()) { + // stack: - + boolean restoreLength = appendPropertyName(ctor, property, memberName); + // stack: - + ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + // stack: ctxt + ctor.visitLdcInsn(getType(realProperty.asGroup().getGroupType().getInterfaceType())); + // stack: ctxt clazz + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "constructGroup", + "(L" + I_CLASS + ";)L" + I_OBJECT + ';', false); + // stack: nested + ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: nested this + ctor.visitInsn(Opcodes.SWAP); + // stack: this nested + ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); + // stack: - + if (restoreLength) { + restoreLength(ctor); + } + } else if (property.isLeaf() || property.isPrimitive() || property.isOptional() && property.isLeaf()) { + // stack: - + ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: this + boolean restoreLength = appendPropertyName(ctor, property, memberName); + ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getConfig", "()L" + I_SMALLRYE_CONFIG + ';', + false); + // stack: this config + ctor.visitVarInsn(Opcodes.ALOAD, V_STRING_BUILDER); + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "toString", "()L" + I_STRING + ';', false); + // stack: this config key + // get the converter to use + ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + // public Converter getValueConverter(Class enclosingType, String field) { + ctor.visitLdcInsn(getType(getInterfaceType())); + ctor.visitLdcInsn(memberName); + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getValueConverter", + "(L" + I_CLASS + ";L" + I_STRING + ";)L" + I_CONVERTER + ';', false); + // stack: this config key converter + Label _try = new Label(); + Label _catch = new Label(); + Label _continue = new Label(); + ctor.visitLabel(_try); + if (property.isOptional()) { + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_SMALLRYE_CONFIG, "getOptionalValue", + "(L" + I_STRING + ";L" + I_CONVERTER + ";)L" + I_OPTIONAL + ';', false); + } else { + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_SMALLRYE_CONFIG, "getValue", + "(L" + I_STRING + ";L" + I_CONVERTER + ";)L" + I_OBJECT + ';', false); + } + // stack: this value + if (property.isPrimitive()) { + PrimitiveProperty prim = property.asPrimitive(); + // unbox it + // stack: this box + String boxType = getInternalName(prim.getBoxType()); + ctor.visitTypeInsn(Opcodes.CHECKCAST, boxType); + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, boxType, prim.getUnboxMethodName(), + prim.getUnboxMethodDescriptor(), false); + // stack: this value + } else if (!property.isOptional()) { + assert property.isLeaf(); + ctor.visitTypeInsn(Opcodes.CHECKCAST, fieldType); + } + // stack: this value + ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); + // stack: - + ctor.visitJumpInsn(Opcodes.GOTO, _continue); + ctor.visitLabel(_catch); + // stack: exception + ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + // stack: exception ctxt + ctor.visitInsn(Opcodes.SWAP); + // stack: ctxt exception + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "reportProblem", + "(L" + I_RUNTIME_EXCEPTION + ";)V", false); + // stack: - + ctor.visitLabel(_continue); + if (restoreLength) { + restoreLength(ctor); + } + // add the try/catch + ctor.visitTryCatchBlock(_try, _catch, _catch, I_RUNTIME_EXCEPTION); + } else if (property.isOptional()) { + // stack: - + ctor.visitMethodInsn(Opcodes.INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false); + // stack: empty + ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: empty this + ctor.visitInsn(Opcodes.SWAP); + // stack: this empty + ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); + + // also generate a sweep-up stub + // stack: - + fio.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + // stack: ctxt + fio.visitLdcInsn(getType(interfaceType)); + // stack: ctxt iface + fio.visitLdcInsn(memberName); + // stack: ctxt iface name + fio.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: ctxt iface name this + fio.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getEnclosedField", + "(L" + I_CLASS + ";L" + I_STRING + ";L" + I_OBJECT + ";)L" + I_OBJECT + ';', false); + // stack: obj? + fio.visitInsn(Opcodes.DUP); + Label _continue = new Label(); + Label _done = new Label(); + // stack: obj? obj? + fio.visitJumpInsn(Opcodes.IFNULL, _continue); + // stack: obj + fio.visitMethodInsn(Opcodes.INVOKESTATIC, I_OPTIONAL, "of", "(L" + I_OBJECT + ";)L" + I_OPTIONAL + ';', false); + // stack: opt + fio.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: opt this + fio.visitInsn(Opcodes.SWAP); + // stack: this opt + fio.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); + // stack: - + fio.visitJumpInsn(Opcodes.GOTO, _done); + fio.visitLabel(_continue); + // stack: null + fio.visitInsn(Opcodes.POP); + // stack: - + fio.visitLabel(_done); + } + + // the accessor method implementation + MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, memberName, "()" + fieldDesc, null, null); + // stack: - + mv.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: this + mv.visitFieldInsn(Opcodes.GETFIELD, className, memberName, fieldDesc); + // stack: obj + if (property.isPrimitive()) { + mv.visitInsn(property.asPrimitive().getReturnInstruction()); + } else { + mv.visitInsn(Opcodes.ARETURN); + } + mv.visitEnd(); + mv.visitMaxs(0, 0); + // end loop + } + // subtype overrides supertype + for (ConfigMappingInterface superType : superTypes) { + superType.addProperties(cv, className, ctor, fio, visited); + } + } + + private boolean appendPropertyName(final MethodVisitor ctor, final Property property, final String memberName) { + if (property.isParentPropertyName()) { + return false; + } + // stack: - + Label _continue = new Label(); + + ctor.visitVarInsn(Opcodes.ALOAD, V_STRING_BUILDER); + + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "length", "()I", false); + // if length != 0 (mean that a prefix exists and not the empty prefix) + ctor.visitJumpInsn(Opcodes.IFEQ, _continue); + + ctor.visitVarInsn(Opcodes.ALOAD, V_STRING_BUILDER); + // stack: sb + ctor.visitLdcInsn('.'); + // stack: sb '.' + ctor.visitInsn(Opcodes.I2C); + // stack: sb '.' + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "append", "(C)L" + I_STRING_BUILDER + ';', false); + + ctor.visitInsn(Opcodes.POP); + + ctor.visitLabel(_continue); + + ctor.visitVarInsn(Opcodes.ALOAD, V_STRING_BUILDER); + + // stack: sb + if (property.hasPropertyName()) { + ctor.visitLdcInsn(property.getPropertyName()); + } else { + ctor.visitLdcInsn(skewer(memberName)); + } + // stack: sb name + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "append", + "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ';', false); + // stack: sb + ctor.visitInsn(Opcodes.POP); + // stack: - + return true; + } + + private void restoreLength(final MethodVisitor ctor) { + // stack: - + ctor.visitVarInsn(Opcodes.ALOAD, V_STRING_BUILDER); + // stack: sb + ctor.visitVarInsn(Opcodes.ILOAD, V_LENGTH); + // stack: sb length + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "setLength", "(I)V", false); + // stack: - + } + + Class createConfigurationObjectClass() { + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + ClassVisitor visitor = usefulDebugInfo ? new Debugging.ClassVisitorImpl(writer) : writer; + + String interfacePackage = interfaceType.getPackage().getName(); + String className = getClass().getPackage().getName() + "." + interfaceType.getSimpleName() + + interfacePackage.hashCode() + "Impl"; + String classInternalName = className.replace('.', '/'); + + visitor.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL, classInternalName, null, I_OBJECT, new String[] { + I_CONFIGURATION_OBJECT, + getInternalName(interfaceType) + }); + visitor.visitSource(null, null); + MethodVisitor ctor = visitor.visitMethod(Opcodes.ACC_PUBLIC, "", "(L" + I_MAPPING_CONTEXT + ";)V", null, null); + ctor.visitParameter("context", Opcodes.ACC_FINAL); + Label ctorStart = new Label(); + Label ctorEnd = new Label(); + ctor.visitLabel(ctorStart); + // stack: - + ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: this + ctor.visitMethodInsn(Opcodes.INVOKESPECIAL, I_OBJECT, "", "()V", false); + // stack: - + ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + // stack: ctxt + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getStringBuilder", "()L" + I_STRING_BUILDER + ';', + false); + // stack: sb + ctor.visitInsn(Opcodes.DUP); + // stack: sb sb + Label ctorSbStart = new Label(); + ctor.visitLabel(ctorSbStart); + ctor.visitVarInsn(Opcodes.ASTORE, V_STRING_BUILDER); + // stack: sb + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "length", "()I", false); + // stack: len + Label ctorLenStart = new Label(); + ctor.visitLabel(ctorLenStart); + ctor.visitVarInsn(Opcodes.ISTORE, V_LENGTH); + // stack: - + MethodVisitor fio = visitor.visitMethod(Opcodes.ACC_PUBLIC, "fillInOptionals", "(L" + I_MAPPING_CONTEXT + ";)V", null, + null); + fio.visitParameter("context", Opcodes.ACC_FINAL); + Label fioStart = new Label(); + Label fioEnd = new Label(); + fio.visitLabel(fioStart); + // stack: - + fio.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + // stack: ctxt + fio.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getStringBuilder", "()L" + I_STRING_BUILDER + ';', + false); + // stack: sb + fio.visitVarInsn(Opcodes.ASTORE, V_STRING_BUILDER); + // stack: - + addProperties(visitor, classInternalName, ctor, fio, new HashSet<>()); + // stack: - + fio.visitInsn(Opcodes.RETURN); + fio.visitLabel(fioEnd); + fio.visitLocalVariable("mc", 'L' + I_MAPPING_CONTEXT + ';', null, fioStart, fioEnd, V_MAPPING_CONTEXT); + fio.visitEnd(); + fio.visitMaxs(0, 0); + // stack: - + ctor.visitInsn(Opcodes.RETURN); + ctor.visitLabel(ctorEnd); + ctor.visitLocalVariable("mc", 'L' + I_MAPPING_CONTEXT + ';', null, ctorStart, ctorEnd, V_MAPPING_CONTEXT); + ctor.visitLocalVariable("sb", 'L' + I_STRING_BUILDER + ';', null, ctorSbStart, ctorEnd, V_STRING_BUILDER); + ctor.visitLocalVariable("len", "I", null, ctorLenStart, ctorEnd, V_LENGTH); + ctor.visitEnd(); + ctor.visitMaxs(0, 0); + visitor.visitEnd(); + + return ClassDefiner.defineClass(MethodHandles.lookup(), getClass(), className, writer.toByteArray()); + } + + private static ConfigMappingInterface[] getSuperTypes(Class[] interfaces, int si, int ti) { + if (si == interfaces.length) { + if (ti == 0) { + return NO_TYPES; + } else { + return new ConfigMappingInterface[ti]; + } + } + Class item = interfaces[si]; + ConfigMappingInterface ci = getConfigurationInterface(item); + if (ci != null) { + ConfigMappingInterface[] array = getSuperTypes(interfaces, si + 1, ti + 1); + array[ti] = ci; + return array; + } else { + return getSuperTypes(interfaces, si + 1, ti); + } + } + + private static Property[] getProperties(Method[] methods, int si, int ti) { + if (si == methods.length) { + if (ti == 0) { + return NO_PROPERTIES; + } else { + return new Property[ti]; + } + } + Method method = methods[si]; + int mods = method.getModifiers(); + if (!Modifier.isPublic(mods) || Modifier.isStatic(mods) || !Modifier.isAbstract(mods)) { + return getProperties(methods, si + 1, ti); + } + if (method.getParameterCount() > 0) { + throw new IllegalArgumentException("Configuration methods cannot accept parameters"); + } + if (method.getReturnType() == void.class) { + throw new IllegalArgumentException("Void config methods are not allowed"); + } + Property p = getPropertyDef(method, method.getGenericReturnType()); + Property[] array = getProperties(methods, si + 1, ti + 1); + array[ti] = p; + return array; + } + + private static Property getPropertyDef(Method method, Type type) { + // now figure out what kind it is + Class> convertWith = getConvertWith(type); + String propertyName = getPropertyName(method); + Class rawType = rawTypeOf(type); + if (rawType.isPrimitive()) { + // primitive! + WithDefault annotation = method.getAnnotation(WithDefault.class); + return new PrimitiveProperty(method, propertyName, rawType, convertWith, + annotation == null ? null : annotation.value()); + } + if (convertWith == null) { + if (rawType == Optional.class) { + // optional is special: it can contain a leaf or a group, but not a map (unless it has @ConvertWith) + Property nested = getPropertyDef(method, typeOfParameter(type, 0)); + if (nested.isMayBeOptional()) { + return new OptionalProperty(method, propertyName, nested.asMayBeOptional()); + } + throw new IllegalArgumentException("Property type " + type + " cannot be optional"); + } + if (rawType == Map.class) { + // it's a map... + Type keyType = typeOfParameter(type, 0); + Class> keyConvertWith = getConvertWith(keyType); + Type valueType = typeOfParameter(type, 1); + return new MapProperty(method, propertyName, keyType, keyConvertWith, getPropertyDef(method, valueType)); + } + ConfigMappingInterface configurationInterface = getConfigurationInterface(rawType); + if (configurationInterface != null) { + // it's a group + return new GroupProperty(method, propertyName, configurationInterface); + } + // fall out (leaf) + } + // otherwise it's a leaf + WithDefault annotation = method.getAnnotation(WithDefault.class); + return new LeafProperty(method, propertyName, type, convertWith, annotation == null ? null : annotation.value()); + } + + private static Class> getConvertWith(final Type type) { + if (type instanceof AnnotatedType) { + WithConverter annotation = ((AnnotatedType) type).getAnnotation(WithConverter.class); + if (annotation != null) { + return annotation.value(); + } else { + return null; + } + } else { + return null; + } + } + + private static String getPropertyName(final AnnotatedElement element) { + boolean useParent = element.getAnnotation(WithParentName.class) != null; + WithName annotation = element.getAnnotation(WithName.class); + if (annotation != null) { + if (useParent) { + throw new IllegalArgumentException("Cannot specify both @ParentConfigName and @ConfigName"); + } + String name = annotation.value(); + if (!name.isEmpty()) { + // already interned, effectively + return name; + } + // else invalid name + throw new IllegalArgumentException("Property name is empty"); + } else if (useParent) { + return ""; + } else { + return null; + } + } + + static Type typeOfParameter(final Type type, final int index) { + if (type instanceof ParameterizedType) { + return ((ParameterizedType) type).getActualTypeArguments()[index]; + } else { + return null; + } + } + + static Class rawTypeOf(final Type type) { + if (type instanceof Class) { + return (Class) type; + } else if (type instanceof ParameterizedType) { + return rawTypeOf(((ParameterizedType) type).getRawType()); + } else if (type instanceof GenericArrayType) { + return Array.newInstance(rawTypeOf(((GenericArrayType) type).getGenericComponentType()), 0).getClass(); + } else if (type instanceof WildcardType) { + Type[] upperBounds = ((WildcardType) type).getUpperBounds(); + if (upperBounds != null) { + return rawTypeOf(upperBounds[0]); + } else { + return Object.class; + } + } else { + throw InjectionMessages.msg.noRawType(type); + } + } + + static final class Debugging { + static StackTraceElement getCaller() { + return new Throwable().getStackTrace()[2]; + } + + static final class MethodVisitorImpl extends MethodVisitor { + + MethodVisitorImpl(final int api) { + super(api); + } + + MethodVisitorImpl(final int api, final MethodVisitor methodVisitor) { + super(api, methodVisitor); + } + + public void visitInsn(final int opcode) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitInsn(opcode); + } + + public void visitIntInsn(final int opcode, final int operand) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitIntInsn(opcode, operand); + } + + public void visitVarInsn(final int opcode, final int var) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitVarInsn(opcode, var); + } + + public void visitTypeInsn(final int opcode, final String type) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitTypeInsn(opcode, type); + } + + public void visitFieldInsn(final int opcode, final String owner, final String name, final String descriptor) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitFieldInsn(opcode, owner, name, descriptor); + } + + public void visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitMethodInsn(opcode, owner, name, descriptor); + } + + public void visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor, + final boolean isInterface) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + + public void visitInvokeDynamicInsn(final String name, final String descriptor, final Handle bootstrapMethodHandle, + final Object... bootstrapMethodArguments) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + } + + public void visitJumpInsn(final int opcode, final Label label) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitJumpInsn(opcode, label); + } + + public void visitLdcInsn(final Object value) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitLdcInsn(value); + } + + public void visitIincInsn(final int var, final int increment) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitIincInsn(var, increment); + } + + public void visitTableSwitchInsn(final int min, final int max, final Label dflt, final Label... labels) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitTableSwitchInsn(min, max, dflt, labels); + } + + public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitLookupSwitchInsn(dflt, keys, labels); + } + + public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions) { + Label l = new Label(); + visitLabel(l); + visitLineNumber(getCaller().getLineNumber(), l); + super.visitMultiANewArrayInsn(descriptor, numDimensions); + } + } + + static final class ClassVisitorImpl extends ClassVisitor { + + final String sourceFile; + + ClassVisitorImpl(final int api) { + super(api); + sourceFile = getCaller().getFileName(); + } + + ClassVisitorImpl(final ClassWriter cw) { + super(Opcodes.ASM7, cw); + sourceFile = getCaller().getFileName(); + } + + public void visitSource(final String source, final String debug) { + super.visitSource(sourceFile, debug); + } + + public MethodVisitor visitMethod(final int access, final String name, final String descriptor, + final String signature, + final String[] exceptions) { + return new MethodVisitorImpl(api, super.visitMethod(access, name, descriptor, signature, exceptions)); + } + } + } +} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingObject.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingObject.java new file mode 100644 index 000000000..daa710847 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingObject.java @@ -0,0 +1,8 @@ +package io.smallrye.config; + +/** + * An interface implemented internally by configuration object implementations. + */ +public interface ConfigMappingObject { + void fillInOptionals(ConfigMappingContext context); +} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java new file mode 100644 index 000000000..d6018203c --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java @@ -0,0 +1,691 @@ +package io.smallrye.config; + +import static io.smallrye.config.ConfigMappingInterface.GroupProperty; +import static io.smallrye.config.ConfigMappingInterface.LeafProperty; +import static io.smallrye.config.ConfigMappingInterface.MapProperty; +import static io.smallrye.config.ConfigMappingInterface.MayBeOptionalProperty; +import static io.smallrye.config.ConfigMappingInterface.PrimitiveProperty; +import static io.smallrye.config.ConfigMappingInterface.Property; +import static io.smallrye.config.ConfigMappingInterface.getConfigurationInterface; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +import org.eclipse.microprofile.config.spi.Converter; + +import io.smallrye.common.constraint.Assert; +import io.smallrye.common.function.Functions; + +/** + * + */ +final class ConfigMappingProvider implements Serializable { + private static final long serialVersionUID = 3977667610888849912L; + + /** + * The do-nothing action is used when the matched property is eager. + */ + private static final BiConsumer DO_NOTHING = Functions.discardingBiConsumer(); + private static final KeyMap> IGNORE_EVERYTHING; + + static { + final KeyMap> map = new KeyMap<>(); + map.putRootValue(DO_NOTHING); + IGNORE_EVERYTHING = map; + } + + private final Map> roots; + private final KeyMap> matchActions; + private final KeyMap defaultValues; + + ConfigMappingProvider(final Builder builder) { + roots = new HashMap<>(builder.roots); + final ArrayDeque currentPath = new ArrayDeque<>(); + KeyMap> matchActions = new KeyMap<>(); + KeyMap defaultValues = new KeyMap<>(); + for (Map.Entry> entry : roots.entrySet()) { + NameIterator rootNi = new NameIterator(entry.getKey()); + while (rootNi.hasNext()) { + final String nextSegment = rootNi.getNextSegment(); + if (!nextSegment.isEmpty()) { + currentPath.add(nextSegment); + } + rootNi.next(); + } + List roots = entry.getValue(); + for (ConfigMappingInterface root : roots) { + // construct the lazy match actions for each group + BiFunction ef = new GetRootAction(root, entry); + processEagerGroup(currentPath, matchActions, defaultValues, root, ef); + } + currentPath.clear(); + } + for (String[] ignoredPath : builder.ignored) { + int len = ignoredPath.length; + KeyMap> found; + if (ignoredPath[len - 1].equals("**")) { + found = matchActions.findOrAdd(ignoredPath, 0, len - 1); + found.putRootValue(DO_NOTHING); + found.putAny(IGNORE_EVERYTHING); + } else { + found = matchActions.findOrAdd(ignoredPath); + found.putRootValue(DO_NOTHING); + } + } + this.matchActions = matchActions; + this.defaultValues = defaultValues; + } + + static String skewer(Method method) { + return skewer(method.getName()); + } + + static String skewer(String camelHumps) { + return skewer(camelHumps, 0, camelHumps.length(), new StringBuilder()); + } + + static String skewer(String camelHumps, int start, int end, StringBuilder b) { + assert !camelHumps.isEmpty() : "Method seems to have an empty name"; + int cp = camelHumps.codePointAt(start); + b.appendCodePoint(Character.toLowerCase(cp)); + start += Character.charCount(cp); + if (start == end) { + // a lonely character at the end of the string + return b.toString(); + } + if (Character.isUpperCase(cp)) { + // all-uppercase words need one code point of lookahead + int nextCp = camelHumps.codePointAt(start); + if (Character.isUpperCase(nextCp)) { + // it's some kind of `WORD` + for (;;) { + b.appendCodePoint(Character.toLowerCase(cp)); + start += Character.charCount(cp); + cp = nextCp; + if (start == end) { + return b.toString(); + } + nextCp = camelHumps.codePointAt(start); + // combine non-letters in with this name + if (Character.isLowerCase(nextCp)) { + b.append('-'); + return skewer(camelHumps, start, end, b); + } + } + // unreachable + } else { + // it was the start of a `Word`; continue until we hit the end or an uppercase. + b.appendCodePoint(nextCp); + start += Character.charCount(nextCp); + for (;;) { + if (start == end) { + return b.toString(); + } + cp = camelHumps.codePointAt(start); + // combine non-letters in with this name + if (Character.isUpperCase(cp)) { + b.append('-'); + return skewer(camelHumps, start, end, b); + } + b.appendCodePoint(cp); + start += Character.charCount(cp); + } + // unreachable + } + // unreachable + } else { + // it's some kind of `word` + for (;;) { + cp = camelHumps.codePointAt(start); + // combine non-letters in with this name + if (Character.isUpperCase(cp)) { + b.append('-'); + return skewer(camelHumps, start, end, b); + } + b.appendCodePoint(cp); + start += Character.charCount(cp); + if (start == end) { + return b.toString(); + } + } + // unreachable + } + // unreachable + } + + static final class ConsumeOneAndThen implements BiConsumer { + private final BiConsumer delegate; + + ConsumeOneAndThen(final BiConsumer delegate) { + this.delegate = delegate; + } + + public void accept(final ConfigMappingContext context, final NameIterator nameIterator) { + nameIterator.previous(); + delegate.accept(context, nameIterator); + nameIterator.next(); + } + } + + static final class ConsumeOneAndThenFn implements BiFunction { + private final BiFunction delegate; + + ConsumeOneAndThenFn(final BiFunction delegate) { + this.delegate = delegate; + } + + public T apply(final ConfigMappingContext context, final NameIterator nameIterator) { + nameIterator.previous(); + T result = delegate.apply(context, nameIterator); + nameIterator.next(); + return result; + } + } + + private void processEagerGroup(final ArrayDeque currentPath, + final KeyMap> matchActions, final KeyMap defaultValues, + final ConfigMappingInterface group, + final BiFunction getEnclosingFunction) { + Class type = group.getInterfaceType(); + int pc = group.getPropertyCount(); + int pathLen = currentPath.size(); + HashSet usedProperties = new HashSet<>(); + for (int i = 0; i < pc; i++) { + Property property = group.getProperty(i); + String memberName = property.getMethod().getName(); + if (usedProperties.add(memberName)) { + // process by property type + if (!property.isParentPropertyName()) { + String propertyName = property.hasPropertyName() ? property.getPropertyName() + : skewer(property.getMethod()); + NameIterator ni = new NameIterator(propertyName); + while (ni.hasNext()) { + currentPath.add(ni.getNextSegment()); + ni.next(); + } + } + if (property.isOptional()) { + // switch to lazy mode + MayBeOptionalProperty nestedProperty = property.asOptional().getNestedProperty(); + if (nestedProperty.isGroup()) { + GroupProperty nestedGroup = nestedProperty.asGroup(); + // on match, always create the outermost group, which recursively creates inner groups + GetOrCreateEnclosingGroupInGroup matchAction = new GetOrCreateEnclosingGroupInGroup( + getEnclosingFunction, group, nestedGroup); + GetFieldOfEnclosing ef = new GetFieldOfEnclosing( + nestedGroup.isParentPropertyName() ? getEnclosingFunction + : new ConsumeOneAndThenFn<>(getEnclosingFunction), + type, memberName); + processLazyGroupInGroup(currentPath, matchActions, defaultValues, nestedGroup, ef, matchAction, + new HashSet<>()); + } else if (nestedProperty.isLeaf()) { + LeafProperty leafProperty = nestedProperty.asLeaf(); + if (leafProperty.hasDefaultValue()) { + defaultValues.findOrAdd(currentPath).putRootValue(leafProperty.getDefaultValue()); + } + matchActions.findOrAdd(currentPath).putRootValue(DO_NOTHING); + } + } else if (property.isGroup()) { + + processEagerGroup(currentPath, matchActions, defaultValues, property.asGroup().getGroupType(), + new GetOrCreateEnclosingGroupInGroup(getEnclosingFunction, group, property.asGroup())); + } else if (property.isPrimitive()) { + // already processed eagerly + PrimitiveProperty primitiveProperty = property.asPrimitive(); + if (primitiveProperty.hasDefaultValue()) { + defaultValues.findOrAdd(currentPath).putRootValue(primitiveProperty.getDefaultValue()); + } + matchActions.findOrAdd(currentPath).putRootValue(DO_NOTHING); + } else if (property.isLeaf()) { + // already processed eagerly + LeafProperty leafProperty = property.asLeaf(); + if (leafProperty.hasDefaultValue()) { + defaultValues.findOrAdd(currentPath).putRootValue(leafProperty.getDefaultValue()); + } + // ignore with no error message + matchActions.findOrAdd(currentPath).putRootValue(DO_NOTHING); + } else if (property.isMap()) { + // the enclosure of the map is this group + processLazyMapInGroup(currentPath, matchActions, defaultValues, property.asMap(), getEnclosingFunction, + group); + } + while (currentPath.size() > pathLen) { + currentPath.removeLast(); + } + } + } + int sc = group.getSuperTypeCount(); + for (int i = 0; i < sc; i++) { + processEagerGroup(currentPath, matchActions, defaultValues, group.getSuperType(i), getEnclosingFunction); + } + } + + private void processLazyGroupInGroup(ArrayDeque currentPath, + KeyMap> matchActions, + KeyMap defaultValues, + GroupProperty groupProperty, + BiFunction getEnclosingFunction, + BiConsumer matchAction, HashSet usedProperties) { + ConfigMappingInterface group = groupProperty.getGroupType(); + int pc = group.getPropertyCount(); + int pathLen = currentPath.size(); + for (int i = 0; i < pc; i++) { + Property property = group.getProperty(i); + if (!property.isParentPropertyName()) { + String propertyName = property.hasPropertyName() ? property.getPropertyName() + : skewer(property.getMethod()); + NameIterator ni = new NameIterator(propertyName); + while (ni.hasNext()) { + currentPath.add(ni.getNextSegment()); + ni.next(); + } + } + if (usedProperties.add(property.getMethod().getName())) { + boolean optional = property.isOptional(); + if (optional && property.asOptional().getNestedProperty().isGroup()) { + GroupProperty nestedGroup = property.asOptional().getNestedProperty().asGroup(); + GetOrCreateEnclosingGroupInGroup nestedMatchAction = new GetOrCreateEnclosingGroupInGroup( + property.isParentPropertyName() ? getEnclosingFunction + : new ConsumeOneAndThenFn<>(getEnclosingFunction), + group, nestedGroup); + processLazyGroupInGroup(currentPath, matchActions, defaultValues, nestedGroup, nestedMatchAction, + nestedMatchAction, new HashSet<>()); + } else if (property.isGroup()) { + GroupProperty asGroup = property.asGroup(); + GetOrCreateEnclosingGroupInGroup nestedEnclosingFunction = new GetOrCreateEnclosingGroupInGroup( + property.isParentPropertyName() ? getEnclosingFunction + : new ConsumeOneAndThenFn<>(getEnclosingFunction), + group, asGroup); + BiConsumer nestedMatchAction; + nestedMatchAction = matchAction; + if (!property.isParentPropertyName()) { + nestedMatchAction = new ConsumeOneAndThen(nestedMatchAction); + } + processLazyGroupInGroup(currentPath, matchActions, defaultValues, asGroup, nestedEnclosingFunction, + nestedMatchAction, usedProperties); + } else if (property.isLeaf() || property.isPrimitive() + || optional && property.asOptional().getNestedProperty().isLeaf()) { + BiConsumer actualAction; + if (!property.isParentPropertyName()) { + actualAction = new ConsumeOneAndThen(matchAction); + } else { + actualAction = matchAction; + } + matchActions.findOrAdd(currentPath).putRootValue(actualAction); + if (property.isPrimitive()) { + PrimitiveProperty primitiveProperty = property.asPrimitive(); + if (primitiveProperty.hasDefaultValue()) { + defaultValues.findOrAdd(currentPath).putRootValue(primitiveProperty.getDefaultValue()); + } + } else if (property.isLeaf()) { + LeafProperty leafProperty = property.asLeaf(); + if (leafProperty.hasDefaultValue()) { + defaultValues.findOrAdd(currentPath).putRootValue(leafProperty.getDefaultValue()); + } + } else { + LeafProperty leafProperty = property.asOptional().getNestedProperty().asLeaf(); + if (leafProperty.hasDefaultValue()) { + defaultValues.findOrAdd(currentPath).putRootValue(leafProperty.getDefaultValue()); + } + } + } else if (property.isMap()) { + processLazyMapInGroup(currentPath, matchActions, defaultValues, property.asMap(), getEnclosingFunction, + group); + } + } + while (currentPath.size() > pathLen) { + currentPath.removeLast(); + } + } + int sc = group.getSuperTypeCount(); + for (int i = 0; i < sc; i++) { + processLazyGroupInGroup(currentPath, matchActions, defaultValues, groupProperty, getEnclosingFunction, + matchAction, usedProperties); + } + } + + private void processLazyMapInGroup(final ArrayDeque currentPath, + final KeyMap> matchActions, final KeyMap defaultValues, + final MapProperty property, BiFunction getEnclosingGroup, + ConfigMappingInterface enclosingGroup) { + GetOrCreateEnclosingMapInGroup getEnclosingMap = new GetOrCreateEnclosingMapInGroup(getEnclosingGroup, enclosingGroup, + property); + processLazyMap(currentPath, matchActions, defaultValues, property, getEnclosingMap, enclosingGroup); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void processLazyMap(final ArrayDeque currentPath, + final KeyMap> matchActions, final KeyMap defaultValues, + final MapProperty property, BiFunction> getEnclosingMap, + ConfigMappingInterface enclosingGroup) { + Property valueProperty = property.getValueProperty(); + Class> keyConvertWith = property.hasKeyConvertWith() ? property.getKeyConvertWith() : null; + Class keyRawType = property.getKeyRawType(); + + if (valueProperty.isLeaf()) { + LeafProperty leafProperty = valueProperty.asLeaf(); + Class> valConvertWith = leafProperty.getConvertWith(); + Class valueRawType = leafProperty.getValueRawType(); + + matchActions.findOrAdd(currentPath).putRootValue((mc, ni) -> { + StringBuilder sb = mc.getStringBuilder(); + sb.setLength(0); + sb.append(ni.getAllPreviousSegments()); + String configKey = sb.toString(); + Map map = getEnclosingMap.apply(mc, ni); + String rawMapKey = ni.getPreviousSegment(); + Converter keyConv; + SmallRyeConfig config = mc.getConfig(); + if (keyConvertWith != null) { + keyConv = mc.getConverterInstance(keyConvertWith); + } else { + keyConv = config.getConverter(keyRawType); + } + Object key = keyConv.convert(rawMapKey); + Converter valueConv; + if (valConvertWith != null) { + valueConv = mc.getConverterInstance(valConvertWith); + } else { + valueConv = config.getConverter(valueRawType); + } + ((Map) map).put(key, config.getValue(configKey, valueConv)); + }); + } else if (valueProperty.isMap()) { + currentPath.addLast("*"); + processLazyMap(currentPath, matchActions, defaultValues, valueProperty.asMap(), (mc, ni) -> { + ni.previous(); + Map enclosingMap = getEnclosingMap.apply(mc, ni); + ni.next(); + String rawMapKey = ni.getPreviousSegment(); + Converter keyConv; + SmallRyeConfig config = mc.getConfig(); + if (keyConvertWith != null) { + keyConv = mc.getConverterInstance(keyConvertWith); + } else { + keyConv = config.getConverter(keyRawType); + } + Object key = keyConv.convert(rawMapKey); + return (Map) ((Map) enclosingMap).computeIfAbsent(key, x -> new HashMap<>()); + }, enclosingGroup); + } else { + assert valueProperty.isGroup(); + currentPath.addLast("*"); + final GetOrCreateEnclosingGroupInMap ef = new GetOrCreateEnclosingGroupInMap(getEnclosingMap, property, + enclosingGroup, valueProperty.asGroup()); + processLazyGroupInGroup(currentPath, matchActions, defaultValues, valueProperty.asGroup(), + ef, ef, new HashSet<>()); + } + currentPath.removeLast(); + } + + static class GetRootAction implements BiFunction { + private final ConfigMappingInterface root; + private final Map.Entry> entry; + + GetRootAction(final ConfigMappingInterface root, final Map.Entry> entry) { + this.root = root; + this.entry = entry; + } + + public ConfigMappingObject apply(final ConfigMappingContext mc, final NameIterator ni) { + return mc + .getRoot(root.getInterfaceType(), entry.getKey()); + } + } + + static class GetOrCreateEnclosingGroupInGroup + implements BiFunction, + BiConsumer { + private final BiFunction delegate; + private final ConfigMappingInterface enclosingGroup; + private final GroupProperty enclosedGroup; + + GetOrCreateEnclosingGroupInGroup(final BiFunction delegate, + final ConfigMappingInterface enclosingGroup, final GroupProperty enclosedGroup) { + this.delegate = delegate; + this.enclosingGroup = enclosingGroup; + this.enclosedGroup = enclosedGroup; + } + + public ConfigMappingObject apply(final ConfigMappingContext context, final NameIterator ni) { + ConfigMappingObject ourEnclosing = delegate.apply(context, ni); + Class enclosingType = enclosingGroup.getInterfaceType(); + String methodName = enclosedGroup.getMethod().getName(); + ConfigMappingObject val = (ConfigMappingObject) context.getEnclosedField(enclosingType, methodName, ourEnclosing); + if (val == null) { + // it must be an optional group + StringBuilder sb = context.getStringBuilder(); + sb.replace(0, sb.length(), ni.getAllPreviousSegments()); + val = (ConfigMappingObject) context.constructGroup(enclosedGroup.getGroupType().getInterfaceType()); + context.registerEnclosedField(enclosingType, methodName, ourEnclosing, val); + } + return val; + } + + public void accept(final ConfigMappingContext context, final NameIterator nameIterator) { + apply(context, nameIterator); + } + } + + static class GetOrCreateEnclosingGroupInMap implements BiFunction, + BiConsumer { + final BiFunction> getEnclosingMap; + final MapProperty enclosingMap; + final ConfigMappingInterface enclosingGroup; + private final GroupProperty enclosedGroup; + + GetOrCreateEnclosingGroupInMap(final BiFunction> getEnclosingMap, + final MapProperty enclosingMap, final ConfigMappingInterface enclosingGroup, + final GroupProperty enclosedGroup) { + this.getEnclosingMap = getEnclosingMap; + this.enclosingMap = enclosingMap; + this.enclosingGroup = enclosingGroup; + this.enclosedGroup = enclosedGroup; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public ConfigMappingObject apply(final ConfigMappingContext context, final NameIterator ni) { + ni.previous(); + Map ourEnclosing = getEnclosingMap.apply(context, ni); + ni.next(); + String mapKey = ni.getPreviousSegment(); + Converter keyConverter = context.getKeyConverter(enclosingGroup.getInterfaceType(), + enclosingMap.getMethod().getName(), enclosingMap.getLevels() - 1); + ConfigMappingObject val = (ConfigMappingObject) ourEnclosing.get(mapKey); + if (val == null) { + StringBuilder sb = context.getStringBuilder(); + sb.replace(0, sb.length(), ni.getAllPreviousSegments()); + Object convertedKey = keyConverter.convert(mapKey); + ((Map) ourEnclosing).put(convertedKey, + val = (ConfigMappingObject) context.constructGroup(enclosedGroup.getGroupType().getInterfaceType())); + } + return val; + } + + public void accept(final ConfigMappingContext context, final NameIterator ni) { + apply(context, ni); + } + } + + static class GetOrCreateEnclosingMapInGroup implements BiFunction>, + BiConsumer { + final BiFunction getEnclosingGroup; + final ConfigMappingInterface enclosingGroup; + final MapProperty property; + + GetOrCreateEnclosingMapInGroup( + final BiFunction getEnclosingGroup, + final ConfigMappingInterface enclosingGroup, final MapProperty property) { + this.getEnclosingGroup = getEnclosingGroup; + this.enclosingGroup = enclosingGroup; + this.property = property; + } + + public Map apply(final ConfigMappingContext context, final NameIterator ni) { + boolean consumeName = !property.isParentPropertyName(); + if (consumeName) + ni.previous(); + ConfigMappingObject ourEnclosing = getEnclosingGroup.apply(context, ni); + if (consumeName) + ni.next(); + Class enclosingType = enclosingGroup.getInterfaceType(); + String methodName = property.getMethod().getName(); + Map val = (Map) context.getEnclosedField(enclosingType, methodName, ourEnclosing); + if (val == null) { + // map is not yet constructed + val = new HashMap<>(); + context.registerEnclosedField(enclosingType, methodName, ourEnclosing, val); + } + return val; + } + + public void accept(final ConfigMappingContext context, final NameIterator ni) { + apply(context, ni); + } + } + + static class GetFieldOfEnclosing implements BiFunction { + private final BiFunction getEnclosingFunction; + private final Class type; + private final String memberName; + + GetFieldOfEnclosing(final BiFunction getEnclosingFunction, + final Class type, final String memberName) { + this.getEnclosingFunction = getEnclosingFunction; + this.type = type; + this.memberName = memberName; + } + + public ConfigMappingObject apply(final ConfigMappingContext mc, final NameIterator ni) { + ConfigMappingObject outer = getEnclosingFunction.apply(mc, ni); + // eagerly populated groups will always exist + return (ConfigMappingObject) mc.getEnclosedField(type, memberName, outer); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + static Map getOrCreateEnclosingMapInMap( + ConfigMappingContext context, NameIterator ni, + BiFunction> getEnclosingMap, ConfigMappingInterface enclosingGroup, + MapProperty property) { + ni.previous(); + Map ourEnclosing = getEnclosingMap.apply(context, ni); + String mapKey = ni.getNextSegment(); + Converter keyConverter = context.getKeyConverter(enclosingGroup.getInterfaceType(), property.getMethod().getName(), + property.getLevels() - 1); + Object realKey = keyConverter.convert(mapKey); + Map map = (Map) ourEnclosing.get(realKey); + if (map == null) { + map = new HashMap<>(); + ((Map) ourEnclosing).put(realKey, map); + } + ni.next(); + return map; + } + + public static Builder builder() { + return new Builder(); + } + + KeyMap getDefaultValues() { + return defaultValues; + } + + ConfigMappings mapConfiguration(SmallRyeConfig config) throws ConfigValidationException { + return mapConfiguration(config, new ConfigMappings()); + } + + ConfigMappings mapConfiguration(SmallRyeConfig config, ConfigMappings mappings) throws ConfigValidationException { + if (roots.isEmpty()) { + return new ConfigMappings(new HashMap<>()); + } + + Assert.checkNotNullParam("config", config); + final ConfigMappingContext context = new ConfigMappingContext(config); + // eagerly populate roots + for (Map.Entry> entry : roots.entrySet()) { + String path = entry.getKey(); + List roots = entry.getValue(); + for (ConfigMappingInterface root : roots) { + StringBuilder sb = context.getStringBuilder(); + sb.replace(0, sb.length(), path); + Class type = root.getInterfaceType(); + ConfigMappingObject group = (ConfigMappingObject) context.constructGroup(type); + context.registerRoot(type, path, group); + } + } + // lazily sweep + for (String name : config.getPropertyNames()) { + // filter properties in root + if (!isPropertyInRoot(name)) { + break; + } + + NameIterator ni = new NameIterator(name); + BiConsumer action = matchActions.findRootValue(ni); + if (action != null) { + action.accept(context, ni); + } else { + context.unknownConfigElement(name); + } + } + ArrayList problems = context.getProblems(); + if (!problems.isEmpty()) { + throw new ConfigValidationException( + problems.toArray(ConfigValidationException.Problem.NO_PROBLEMS)); + } + context.fillInOptionals(); + + mappings.registerConfigMappings(context.getRootsMap()); + return mappings; + } + + private boolean isPropertyInRoot(String propertyName) { + final Set registeredRoots = roots.keySet(); + for (String registeredRoot : registeredRoots) { + if (propertyName.startsWith(registeredRoot)) { + return true; + } + } + return false; + } + + public static final class Builder { + final Map> roots = new HashMap<>(); + final List ignored = new ArrayList<>(); + + Builder() { + } + + public Builder addRoot(String path, Class type) { + Assert.checkNotNullParam("type", type); + return addRoot(path, getConfigurationInterface(type)); + } + + public Builder addRoot(String path, ConfigMappingInterface info) { + Assert.checkNotNullParam("path", path); + Assert.checkNotNullParam("info", info); + roots.computeIfAbsent(path, k -> new ArrayList<>(4)).add(info); + return this; + } + + public Builder addIgnored(String path) { + Assert.checkNotNullParam("path", path); + ignored.add(path.split("\\.")); + return this; + } + + public ConfigMappingProvider build() { + 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 new file mode 100644 index 000000000..42920f7db --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappings.java @@ -0,0 +1,32 @@ +package io.smallrye.config; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +final class ConfigMappings implements Serializable { + private static final long serialVersionUID = -7790784345796818526L; + + private final Map, Map> mappings; + + ConfigMappings() { + this(new HashMap<>()); + } + + ConfigMappings(final Map, Map> mappings) { + this.mappings = mappings; + } + + void registerConfigMappings(Map, Map> mappings) { + this.mappings.putAll(mappings); + } + + T getConfigMapping(Class type, String prefix) { + final ConfigMappingObject configMappingObject = mappings.getOrDefault(type, Collections.emptyMap()).get(prefix); + if (configMappingObject == null) { + throw ConfigMessages.msg.mappingNotFound(type.getName(), prefix); + } + return type.cast(configMappingObject); + } +} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMessages.java b/implementation/src/main/java/io/smallrye/config/ConfigMessages.java index c560a6fdb..6b775b668 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMessages.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMessages.java @@ -94,4 +94,7 @@ interface ConfigMessages { @Message(id = 26, value = "%s cannot be converted into a UUID") IllegalArgumentException malformedUUID(@Cause Throwable cause, String malformedUUID); + + @Message(id = 27, value = "Could not find a mapping for %s with prefix %s") + NoSuchElementException mappingNotFound(String className, String prefix); } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValidationException.java b/implementation/src/main/java/io/smallrye/config/ConfigValidationException.java new file mode 100644 index 000000000..647be214b --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigValidationException.java @@ -0,0 +1,57 @@ +package io.smallrye.config; + +import io.smallrye.common.constraint.Assert; + +/** + * An exception which is thrown when a configuration validation problem occurs. + */ +public class ConfigValidationException extends Exception { + private static final long serialVersionUID = -2637730579475070264L; + + private final Problem[] problems; + + /** + * Constructs a new {@code ConfigurationValidationException} instance. + * + * @param problems the reported problems + */ + public ConfigValidationException(final Problem[] problems) { + super(list("Configuration validation failed", problems)); + this.problems = problems; + } + + private static String list(String msg, Problem[] problems) { + StringBuilder b = new StringBuilder(); + b.append(msg).append(':'); + for (int i = 0; i < problems.length; i++) { + Problem problem = problems[i]; + Assert.checkNotNullArrayParam("problems", i, problem); + b.append(System.lineSeparator()); + b.append("\t"); + b.append(problem.getMessage()); + } + return b.toString(); + } + + public int getProblemCount() { + return problems.length; + } + + public Problem getProblem(int index) { + return problems[index]; + } + + public static final class Problem { + public static final Problem[] NO_PROBLEMS = new Problem[0]; + + private final String message; + + public Problem(final String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } +} diff --git a/implementation/src/main/java/io/smallrye/config/KeyMap.java b/implementation/src/main/java/io/smallrye/config/KeyMap.java new file mode 100644 index 000000000..2ea85c7db --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/KeyMap.java @@ -0,0 +1,259 @@ +package io.smallrye.config; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +import io.smallrye.common.function.Functions; + +/** + * A multi-level key map. + */ +public final class KeyMap extends HashMap> { + private static final Object NO_VALUE = new Serializable() { + private static final long serialVersionUID = -6072559389176920349L; + }; + + private static final long serialVersionUID = 3584966224369608557L; + + private KeyMap any; + @SuppressWarnings("unchecked") + private V rootValue = (V) NO_VALUE; + + public KeyMap(final int initialCapacity, final float loadFactor) { + super(initialCapacity, loadFactor); + } + + public KeyMap(final int initialCapacity) { + super(initialCapacity); + } + + public KeyMap() { + } + + public KeyMap get(final String key, int offs, int len) { + return get(key.substring(offs, offs + len)); + } + + public KeyMap getAny() { + return any; + } + + public KeyMap getOrCreateAny() { + KeyMap any = this.any; + if (any == null) { + any = this.any = new KeyMap<>(); + } + return any; + } + + public KeyMap putAny(KeyMap any) { + KeyMap oldAny = this.any; + this.any = any; + return oldAny; + } + + public KeyMap putAnyIfAbsent(KeyMap any) { + KeyMap oldAny = this.any; + if (oldAny == null) { + this.any = any; + return null; + } else { + return oldAny; + } + } + + public boolean hasRootValue() { + return rootValue != NO_VALUE; + } + + public V getRootValue() { + return getRootValueOrDefault(null); + } + + public V getRootValueOrDefault(final V defaultVal) { + V rootValue = this.rootValue; + return rootValue == NO_VALUE ? defaultVal : rootValue; + } + + public V getOrComputeRootValue(final Supplier supplier) { + V rootValue = this.rootValue; + if (rootValue == NO_VALUE) { + this.rootValue = rootValue = supplier.get(); + } + return rootValue; + } + + @SuppressWarnings("unchecked") + public V removeRootValue() { + V rootValue = this.rootValue; + if (rootValue != NO_VALUE) { + this.rootValue = (V) NO_VALUE; + } + return rootValue; + } + + public V putRootValue(final V rootValue) { + V old = this.rootValue; + this.rootValue = rootValue; + return old == NO_VALUE ? null : old; + } + + public KeyMap find(final String path) { + return find(new NameIterator(path)); + } + + public KeyMap find(final NameIterator ni) { + if (!ni.hasNext()) { + return this; + } + String seg = ni.getNextSegment(); + ni.next(); + KeyMap next = getOrDefault(seg, any); + return next == null ? null : next.find(ni); + } + + public KeyMap find(final Iterator iter) { + if (!iter.hasNext()) { + return this; + } + String seg = iter.next(); + KeyMap next = seg.equals("*") ? any : getOrDefault(seg, any); + return next == null ? null : next.find(iter); + } + + public KeyMap find(final Iterable i) { + return find(i.iterator()); + } + + public KeyMap findOrAdd(final String path) { + return findOrAdd(new NameIterator(path)); + } + + public KeyMap findOrAdd(final NameIterator ni) { + if (!ni.hasNext()) { + return this; + } + String seg = ni.getNextSegment(); + ni.next(); + try { + KeyMap next = seg.equals("*") ? getOrCreateAny() : computeIfAbsent(seg, k -> new KeyMap<>()); + return next.findOrAdd(ni); + } finally { + ni.previous(); + } + } + + public KeyMap findOrAdd(final Iterator iter) { + if (!iter.hasNext()) { + return this; + } + String seg = iter.next(); + KeyMap next = seg.equals("*") ? getOrCreateAny() : computeIfAbsent(seg, k -> new KeyMap<>()); + return next.findOrAdd(iter); + } + + public KeyMap findOrAdd(final Iterable i) { + return findOrAdd(i.iterator()); + } + + public KeyMap findOrAdd(final String... keys) { + return findOrAdd(keys, 0, keys.length); + } + + public KeyMap findOrAdd(final String[] keys, int off, int len) { + String seg = keys[off]; + KeyMap next = seg.equals("*") ? getOrCreateAny() : computeIfAbsent(seg, k -> new KeyMap<>()); + return off + 1 > len - 1 ? next : next.findOrAdd(keys, off + 1, len); + } + + public V findRootValue(final String path) { + return findRootValue(new NameIterator(path)); + } + + public V findRootValue(final NameIterator ni) { + KeyMap result = find(ni); + return result == null ? null : result.getRootValue(); + } + + public boolean hasRootValue(final String path) { + return hasRootValue(new NameIterator(path)); + } + + public boolean hasRootValue(final NameIterator ni) { + KeyMap result = find(ni); + return result != null && result.hasRootValue(); + } + + public KeyMap map(P param, BiFunction conversion) { + return map(param, conversion, new IdentityHashMap<>()); + } + + public KeyMap map(Function conversion) { + return map(conversion, Functions.functionBiFunction()); + } + + KeyMap map(P param, BiFunction conversion, IdentityHashMap, KeyMap> cached) { + if (cached.containsKey(this)) { + return cached.get(this); + } + KeyMap newMap = new KeyMap<>(size()); + cached.put(this, newMap); + Set>> entries = entrySet(); + for (Entry> entry : entries) { + newMap.put(entry.getKey(), entry.getValue().map(param, conversion, cached)); + } + KeyMap any = getAny(); + if (any != null) { + newMap.putAny(any.map(param, conversion, cached)); + } + if (hasRootValue()) { + newMap.putRootValue(conversion.apply(param, getRootValue())); + } + return newMap; + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public StringBuilder toString(StringBuilder b) { + b.append("KeyMap("); + V rootValue = this.rootValue; + if (rootValue == NO_VALUE) { + b.append("no value"); + } else { + b.append("value=").append(rootValue); + } + b.append(") {"); + final Iterator>> iterator = entrySet().iterator(); + KeyMap any = this.any; + if (iterator.hasNext()) { + Entry> entry = iterator.next(); + b.append(entry.getKey()).append("=>"); + entry.getValue().toString(b); + while (iterator.hasNext()) { + entry = iterator.next(); + b.append(',').append(entry.getKey()).append("=>"); + entry.getValue().toString(b); + } + if (any != null) { + b.append(',').append("(any)=>"); + any.toString(b); + } + } else { + if (any != null) { + b.append("(any)=>"); + any.toString(b); + } + } + b.append('}'); + return b; + } + + public String toString() { + return toString(new StringBuilder()).toString(); + } +} diff --git a/implementation/src/main/java/io/smallrye/config/KeyMapBackedConfigSource.java b/implementation/src/main/java/io/smallrye/config/KeyMapBackedConfigSource.java new file mode 100644 index 000000000..b985d5f27 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/KeyMapBackedConfigSource.java @@ -0,0 +1,34 @@ +package io.smallrye.config; + +import java.util.Collections; +import java.util.Map; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.config.common.AbstractConfigSource; + +public class KeyMapBackedConfigSource extends AbstractConfigSource { + private static final long serialVersionUID = 4378754290346888762L; + + private final KeyMap properties; + + public KeyMapBackedConfigSource(final String name, final KeyMap properties) { + super(name, ConfigSource.DEFAULT_ORDINAL); + this.properties = properties; + } + + public KeyMapBackedConfigSource(final String name, final int ordinal, final KeyMap properties) { + super(name, ordinal); + this.properties = properties; + } + + @Override + public Map getProperties() { + return Collections.emptyMap(); + } + + @Override + public String getValue(final String propertyName) { + return properties.findRootValue(propertyName); + } +} diff --git a/implementation/src/main/java/io/smallrye/config/NameIterator.java b/implementation/src/main/java/io/smallrye/config/NameIterator.java new file mode 100644 index 000000000..5c450857b --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/NameIterator.java @@ -0,0 +1,350 @@ +package io.smallrye.config; + +import java.util.NoSuchElementException; + +import io.smallrye.common.constraint.Assert; + +/** + * An iterator for property name strings. + */ +public final class NameIterator { + /** + * Configuration key maximum allowed length. + */ + public static final int MAX_LENGTH = 2048; + private static final int POS_MASK = 0x0FFF; + private static final int POS_BITS = 12; + private static final int SE_SHIFT = 32 - POS_BITS; + + private final String name; + private int pos; + + public NameIterator(final String name) { + this(name, false); + } + + public NameIterator(final String name, final boolean startAtEnd) { + this(name, startAtEnd ? name.length() : -1); + } + + public NameIterator(final String name, final int pos) { + Assert.checkNotNullParam("name", name); + if (name.length() > MAX_LENGTH) + throw new IllegalArgumentException("Name is too long"); + Assert.checkMinimumParameter("pos", -1, pos); + Assert.checkMaximumParameter("pos", name.length(), pos); + if (pos != -1 && pos != name.length() && name.charAt(pos) != '.') + throw new IllegalArgumentException("Position is not located at a delimiter"); + this.name = name; + this.pos = pos; + } + + public void goToEnd() { + this.pos = name.length(); + } + + public void goToStart() { + this.pos = -1; + } + + /** + * Get the cursor position. It will be {@code -1} if the cursor is at the beginning of the string, or {@code name.length()} + * if it is at the end. + * + * @return the cursor position + */ + public int getPosition() { + return pos; + } + + /* + * next-iteration DFA + * ## on EOI + * I → ## on '.' + * I → Q ## on '"' + * Q → I ## on '"' + * Q → QBS ## on '\' + * QBS → Q ## on any + * I → BS ## on '\' + * BS → I ## on any + */ + private static final int FS_INITIAL = 0; + private static final int FS_QUOTE = 1; + private static final int FS_BACKSLASH = 2; + private static final int FS_QUOTE_BACKSLASH = 3; + + //@formatter:off + /* + * Iteration cookie format + * + * Bit: 14...12 11 ... 0 + * ┌───────┬────────────┐ + * │ state │ position │ + * │ │ (signed) │ + * └───────┴────────────┘ + */ + //@formatter:on + + /** + * Create a new iteration cookie at the current position. + * + * @return the new cookie + */ + private int initIteration() { + return this.pos & POS_MASK; + } + + private int cookieOf(int state, int pos) { + return state << POS_BITS | pos & POS_MASK; + } + + private int getPosition(int cookie) { + return (cookie & POS_MASK) << SE_SHIFT >> SE_SHIFT; + } + + private int getState(int cookie) { + return cookie >> POS_BITS; + } + + /** + * Move to the next position. + * + * @param cookie the original cookie value + * @return the new cookie value + */ + private int nextPos(int cookie) { + int pos = getPosition(cookie); + if (isEndOfString(cookie)) { + throw new NoSuchElementException(); + } + int state = getState(cookie); + int ch; + for (;;) { + pos++; + if (pos == name.length()) { + return cookieOf(state, pos); + } + ch = name.charAt(pos); + if (state == FS_INITIAL) { + if (ch == '.') { + return cookieOf(state, pos); + } else if (ch == '"') { + state = FS_QUOTE; + } else if (ch == '\\') { + state = FS_BACKSLASH; + } else { + return cookieOf(state, pos); + } + } else if (state == FS_QUOTE) { + if (ch == '"') { + state = FS_INITIAL; + } else if (ch == '\\') { + state = FS_QUOTE_BACKSLASH; + } else { + return cookieOf(state, pos); + } + } else if (state == FS_BACKSLASH) { + state = FS_INITIAL; + return cookieOf(state, pos); + } else { + assert state == FS_QUOTE_BACKSLASH; + state = FS_QUOTE; + return cookieOf(state, pos); + } + } + } + + private int prevPos(int cookie) { + int pos = getPosition(cookie); + if (isStartOfString(cookie)) { + throw new NoSuchElementException(); + } + int state = getState(cookie); + int ch; + for (;;) { + pos--; + if (pos == -1) { + return cookieOf(state, pos); + } + ch = name.charAt(pos); + if (state == FS_INITIAL) { + if (pos >= 1 && name.charAt(pos - 1) == '\\') { + // always accept as-is + return cookieOf(state, pos); + } else if (ch == '.') { + return cookieOf(state, pos); + } else if (ch == '"') { + state = FS_QUOTE; + } else if (ch == '\\') { + // skip + } else { + // regular char + return cookieOf(state, pos); + } + } else if (state == FS_QUOTE) { + if (pos >= 1 && name.charAt(pos - 1) == '\\') { + // always accept as-is + return cookieOf(state, pos); + } else if (ch == '"') { + state = FS_INITIAL; + } else if (ch == '\\') { + // skip + } else { + return cookieOf(state, pos); + } + } else { + throw Assert.unreachableCode(); + } + } + } + + private boolean isSegmentDelimiter(int cookie) { + return isStartOfString(cookie) || isEndOfString(cookie) || getState(cookie) == FS_INITIAL && charAt(cookie) == '.'; + } + + private boolean isEndOfString(int cookie) { + return getPosition(cookie) == name.length(); + } + + private boolean isStartOfString(int cookie) { + return getPosition(cookie) == -1; + } + + private int charAt(int cookie) { + return name.charAt(getPosition(cookie)); + } + + public int getPreviousStart() { + int cookie = initIteration(); + do { + cookie = prevPos(cookie); + } while (!isSegmentDelimiter(cookie)); + return getPosition(cookie) + 1; + } + + public int getNextEnd() { + int cookie = initIteration(); + do { + cookie = nextPos(cookie); + } while (!isSegmentDelimiter(cookie)); + return getPosition(cookie); + } + + public boolean nextSegmentEquals(String other) { + return nextSegmentEquals(other, 0, other.length()); + } + + public boolean nextSegmentEquals(String other, int offs, int len) { + int cookie = initIteration(); + int strPos = 0; + for (;;) { + cookie = nextPos(cookie); + if (isSegmentDelimiter(cookie)) { + return strPos == len; + } + if (strPos == len) { + return false; + } + if (other.charAt(offs + strPos) != charAt(cookie)) { + return false; + } + strPos++; + } + } + + public String getNextSegment() { + final StringBuilder b = new StringBuilder(); + int cookie = initIteration(); + for (;;) { + cookie = nextPos(cookie); + if (isSegmentDelimiter(cookie)) { + return b.toString(); + } + b.append((char) charAt(cookie)); + } + } + + public boolean previousSegmentEquals(String other) { + return previousSegmentEquals(other, 0, other.length()); + } + + public boolean previousSegmentEquals(final String other, final int offs, final int len) { + int cookie = initIteration(); + int strPos = len; + for (;;) { + strPos--; + cookie = prevPos(cookie); + if (isSegmentDelimiter(cookie)) { + return strPos == -1; + } + if (strPos == -1) { + return false; + } + if (other.charAt(offs + strPos) != charAt(cookie)) { + return false; + } + } + } + + public String getPreviousSegment() { + final StringBuilder b = new StringBuilder(); + int cookie = initIteration(); + for (;;) { + cookie = prevPos(cookie); + if (isSegmentDelimiter(cookie)) { + return b.reverse().toString(); + } + b.append((char) charAt(cookie)); + } + } + + public String getAllPreviousSegments() { + final int pos = getPosition(); + if (pos == -1) { + return ""; + } + return name.substring(0, pos); + } + + public String getAllPreviousSegmentsWith(String suffix) { + final int pos = getPosition(); + if (pos == -1) { + return suffix; + } + return name.substring(0, pos) + "." + suffix; + } + + public boolean hasNext() { + return pos < name.length(); + } + + public boolean hasPrevious() { + return pos > -1; + } + + public void next() { + pos = getNextEnd(); + } + + public void previous() { + pos = getPreviousStart() - 1; + } + + public String getName() { + return name; + } + + public String toString() { + if (pos == -1) { + return "*" + name; + } else if (pos == name.length()) { + return name + "*"; + } else { + return name.substring(0, pos) + '*' + name.substring(pos + 1); + } + } + + public void appendTo(final StringBuilder sb) { + sb.append(getAllPreviousSegments()); + } +} diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java index 44bff1143..eeca7c1b8 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java @@ -45,7 +45,6 @@ import io.smallrye.common.annotation.Experimental; import io.smallrye.config.SmallRyeConfigBuilder.InterceptorWithPriority; -import io.smallrye.config.common.MapBackedConfigSource; /** * @author Jeff Mesnil (c) 2017 Red Hat inc. @@ -57,9 +56,12 @@ public class SmallRyeConfig implements Config, Serializable { private final Map> converters; private final Map>> optionalConverters = new ConcurrentHashMap<>(); - SmallRyeConfig(SmallRyeConfigBuilder builder) { + private final ConfigMappings mappings; + + SmallRyeConfig(SmallRyeConfigBuilder builder, ConfigMappings mappings) { this.configSources = new AtomicReference<>(new ConfigSources(buildConfigSources(builder), buildInterceptors(builder))); this.converters = buildConverters(builder); + this.mappings = mappings; } @Deprecated @@ -68,6 +70,7 @@ protected SmallRyeConfig(List configSources, Map(Converters.ALL_CONVERTERS); this.converters.putAll(converters); + this.mappings = new ConfigMappings(); } private List buildConfigSources(final SmallRyeConfigBuilder builder) { @@ -78,16 +81,6 @@ private List buildConfigSources(final SmallRyeConfigBuilder builde if (builder.isAddDefaultSources()) { sourcesToBuild.addAll(builder.getDefaultSources()); } - if (!builder.getDefaultValues().isEmpty()) { - sourcesToBuild.add(new MapBackedConfigSource("DefaultValuesConfigSource", builder.getDefaultValues()) { - private static final long serialVersionUID = -2569643736033594267L; - - @Override - public int getOrdinal() { - return Integer.MIN_VALUE; - } - }); - } // wrap all final Function sourceWrappersToBuild = builder.getSourceWrappers(); @@ -211,6 +204,16 @@ public > Optional getOptionalValues(String name, C return getOptionalValue(name, Converters.newCollectionConverter(converter, collectionFactory)); } + @Experimental("TODO") + public T getConfigMapping(Class type) { + return getConfigMapping(type, ""); + } + + @Experimental("TODO") + public T getConfigMapping(Class type, String prefix) { + return mappings.getConfigMapping(type, prefix); + } + @Override public Iterable getPropertyNames() { final HashSet names = new HashSet<>(); diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java index d1823edbb..dce4a71a6 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java @@ -56,6 +56,7 @@ public class SmallRyeConfigBuilder implements ConfigBuilder { private final Set secretKeys = new HashSet<>(); private final List interceptors = new ArrayList<>(); private final Map defaultValues = new HashMap<>(); + private final ConfigMappingProvider.Builder mappingsBuilder = ConfigMappingProvider.builder(); private ClassLoader classLoader = SecuritySupport.getContextClassLoader(); private boolean addDefaultSources = false; private boolean addDefaultInterceptors = false; @@ -224,6 +225,20 @@ public SmallRyeConfigBuilder withDefaultValues(Map defaultValues return this; } + public SmallRyeConfigBuilder withMapping(Class klass) { + return withMapping(klass, ""); + } + + public SmallRyeConfigBuilder withMapping(Class klass, String prefix) { + mappingsBuilder.addRoot(prefix, klass); + return this; + } + + public SmallRyeConfigBuilder withMappingIgnore(String path) { + mappingsBuilder.addIgnored(path); + return this; + } + @Override public SmallRyeConfigBuilder withConverters(Converter[] converters) { for (Converter converter : converters) { @@ -314,7 +329,22 @@ boolean isAddDiscoveredInterceptors() { @Override public SmallRyeConfig build() { - return new SmallRyeConfig(this); + ConfigMappingProvider mappingProvider = mappingsBuilder.build(); + if (!defaultValues.isEmpty() || !mappingProvider.getDefaultValues().isEmpty()) { + final KeyMap mappingProviderDefaultValues = mappingProvider.getDefaultValues(); + defaultValues.forEach((key, value) -> mappingProviderDefaultValues.findOrAdd(key).putRootValue(value)); + withSources( + new KeyMapBackedConfigSource("DefaultValuesConfigSource", Integer.MIN_VALUE, mappingProviderDefaultValues)); + } + + try { + ConfigMappings configMappings = new ConfigMappings(); + SmallRyeConfig config = new SmallRyeConfig(this, configMappings); + mappingProvider.mapConfiguration(config, configMappings); + return config; + } catch (ConfigValidationException e) { + throw new IllegalStateException(e); + } } static class ConverterWithPriority { diff --git a/implementation/src/main/java/io/smallrye/config/WithConverter.java b/implementation/src/main/java/io/smallrye/config/WithConverter.java new file mode 100644 index 000000000..cf8a5b603 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/WithConverter.java @@ -0,0 +1,24 @@ +package io.smallrye.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.microprofile.config.spi.Converter; + +/** + * Specify the converter to use to convert the annotated type. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE_USE) +public @interface WithConverter { + /** + * The converter class to use. + * + * @return the converter class + */ + Class> value(); +} diff --git a/implementation/src/main/java/io/smallrye/config/WithDefault.java b/implementation/src/main/java/io/smallrye/config/WithDefault.java new file mode 100644 index 000000000..5051fe6cc --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/WithDefault.java @@ -0,0 +1,17 @@ +package io.smallrye.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specify the default value of a property. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface WithDefault { + String value(); +} diff --git a/implementation/src/main/java/io/smallrye/config/WithName.java b/implementation/src/main/java/io/smallrye/config/WithName.java new file mode 100644 index 000000000..880188223 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/WithName.java @@ -0,0 +1,22 @@ +package io.smallrye.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The name of the configuration property or group. + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface WithName { + /** + * The name of the property or group. Must not be empty. + * + * @return the name + */ + String value(); +} diff --git a/implementation/src/main/java/io/smallrye/config/WithParentName.java b/implementation/src/main/java/io/smallrye/config/WithParentName.java new file mode 100644 index 000000000..a0769c30d --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/WithParentName.java @@ -0,0 +1,16 @@ +package io.smallrye.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use the parent's configuration name. + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface WithParentName { +} diff --git a/implementation/src/main/java/io/smallrye/config/inject/ConfigExtension.java b/implementation/src/main/java/io/smallrye/config/inject/ConfigExtension.java index 2937880d5..38397f9f7 100644 --- a/implementation/src/main/java/io/smallrye/config/inject/ConfigExtension.java +++ b/implementation/src/main/java/io/smallrye/config/inject/ConfigExtension.java @@ -29,7 +29,9 @@ import java.util.function.Supplier; import java.util.stream.StreamSupport; +import javax.enterprise.context.Dependent; import javax.enterprise.event.Observes; +import javax.enterprise.inject.Default; import javax.enterprise.inject.Instance; import javax.enterprise.inject.spi.AfterBeanDiscovery; import javax.enterprise.inject.spi.AfterDeploymentValidation; @@ -38,7 +40,9 @@ import javax.enterprise.inject.spi.BeforeBeanDiscovery; import javax.enterprise.inject.spi.Extension; import javax.enterprise.inject.spi.InjectionPoint; +import javax.enterprise.inject.spi.ProcessAnnotatedType; import javax.enterprise.inject.spi.ProcessInjectionPoint; +import javax.enterprise.inject.spi.WithAnnotations; import javax.inject.Provider; import org.eclipse.microprofile.config.Config; @@ -46,6 +50,7 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.spi.Converter; +import io.smallrye.config.ConfigMapping; import io.smallrye.config.ConfigValue; import io.smallrye.config.SecretKeys; import io.smallrye.config.SmallRyeConfig; @@ -57,6 +62,7 @@ */ public class ConfigExtension implements Extension { private final Set injectionPoints = new HashSet<>(); + private final Set> configMappings = new HashSet<>(); public ConfigExtension() { } @@ -66,6 +72,11 @@ protected void beforeBeanDiscovery(@Observes BeforeBeanDiscovery bbd, BeanManage bbd.addAnnotatedType(configBean, ConfigProducer.class.getName()); } + protected void processConfigMappings( + @Observes @WithAnnotations({ ConfigMapping.class }) ProcessAnnotatedType processAnnotatedType) { + configMappings.add(processAnnotatedType.getAnnotatedType()); + } + protected void collectConfigPropertyInjectionPoints(@Observes ProcessInjectionPoint pip) { if (pip.getInjectionPoint().getAnnotated().isAnnotationPresent(ConfigProperty.class)) { injectionPoints.add(pip.getInjectionPoint()); @@ -96,6 +107,22 @@ protected void registerCustomBeans(@Observes AfterBeanDiscovery abd, BeanManager for (Class customType : customTypes) { abd.addBean(new ConfigInjectionBean<>(bm, customType)); } + + SmallRyeConfig config = (SmallRyeConfig) ConfigProvider.getConfig(getContextClassLoader()); + for (AnnotatedType configMapping : configMappings) { + abd.addBean() + .id(configMapping.getJavaClass().toString()) + .beanClass(configMapping.getJavaClass()) + .types(configMapping.getJavaClass()) + .qualifiers(Default.Literal.INSTANCE) + .scope(Dependent.class) + .createWith(creationalContext -> { + String prefix = Optional.ofNullable(configMapping.getAnnotation(ConfigMapping.class)) + .map(ConfigMapping::value) + .orElse(""); + return config.getConfigMapping(configMapping.getJavaClass(), prefix); + }); + } } protected void validate(@Observes AfterDeploymentValidation adv) { diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingProviderTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingProviderTest.java new file mode 100644 index 000000000..a59fee94b --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingProviderTest.java @@ -0,0 +1,467 @@ +package io.smallrye.config; + +import static io.smallrye.config.KeyValuesConfigSource.config; +import static java.util.stream.Collectors.toList; +import static java.util.stream.StreamSupport.stream; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.stream.Stream; + +import org.eclipse.microprofile.config.spi.Converter; +import org.junit.jupiter.api.Test; + +import io.smallrye.config.common.MapBackedConfigSource; + +public class ConfigMappingProviderTest { + @Test + void configMapping() { + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Server.class, "server") + .withSources(config("server.host", "localhost", "server.port", "8080")).build(); + final Server configProperties = config.getConfigMapping(Server.class, "server"); + assertEquals("localhost", configProperties.host()); + assertEquals(8080, configProperties.port()); + } + + @Test + void noConfigMapping() { + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("server.host", "localhost", "server.port", "8080")).build(); + assertThrows(NoSuchElementException.class, () -> config.getConfigMapping(Server.class, "server"), + "Could not find a mapping for " + Server.class.getName() + " with prefix server"); + } + + @Test + void unregisteredConfigMapping() { + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("host", "localhost", "port", "8080")).build(); + assertThrows(NoSuchElementException.class, () -> config.getConfigMapping(Server.class), + "Could not find a mapping for " + Server.class.getName() + "with prefix"); + } + + @Test + void noPrefix() { + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Server.class) + .withSources(config("host", "localhost", "port", "8080")).build(); + final Server configProperties = config.getConfigMapping(Server.class); + assertEquals("localhost", configProperties.host()); + assertEquals(8080, configProperties.port()); + } + + @Test + void configMappingBuilder() throws Exception { + final ConfigMappingProvider configMappingProvider = ConfigMappingProvider.builder().addRoot("server", Server.class) + .addIgnored("server.name").build(); + final SmallRyeConfig config = new SmallRyeConfigBuilder().withSources( + config("server.host", "localhost", "server.port", "8080", "server.name", "name")).build(); + + final Server server = configMappingProvider.mapConfiguration(config).getConfigMapping(Server.class, "server"); + assertEquals("localhost", server.host()); + assertEquals(8080, server.port()); + } + + @Test + void unknownConfigElement() { + assertThrows(IllegalStateException.class, + () -> new SmallRyeConfigBuilder().withMapping(Server.class, "server").build()); + } + + @Test + void ignorePropertiesInUnregisteredRoots() { + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Server.class, "server") + .withSources(config("server.host", "localhost", "server.port", "8080", "client.name", "konoha")) + .build(); + final Server configProperties = config.getConfigMapping(Server.class, "server"); + assertEquals("localhost", configProperties.host()); + assertEquals(8080, configProperties.port()); + } + + @Test + void ignoreSomeProperties() { + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Server.class, "server") + .withMapping(Client.class, "client") + .withMappingIgnore("client.**") + .withSources(config("server.host", "localhost", "server.port", "8080", "client.host", "localhost", + "client.port", "8080", "client.name", "konoha")) + .build(); + + final Server server = config.getConfigMapping(Server.class, "server"); + assertEquals("localhost", server.host()); + assertEquals(8080, server.port()); + + final Client client = config.getConfigMapping(Client.class, "client"); + assertEquals("localhost", client.host()); + assertEquals(8080, client.port()); + } + + @Test + void ignoreProperties() { + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultSources() + .withMapping(Server.class, "server") + .withSources(config("server.host", "localhost", "server.port", "8080")).build(); + final Server configProperties = config.getConfigMapping(Server.class, "server"); + assertEquals("localhost", configProperties.host()); + assertEquals(8080, configProperties.port()); + } + + @Test + void splitRoots() throws Exception { + final SmallRyeConfig config = new SmallRyeConfigBuilder().withSources( + config("server.host", "localhost", "server.port", "8080", "server.name", "konoha")) + .build(); + + final ConfigMappingProvider configMappingProvider = ConfigMappingProvider.builder() + .addRoot("server", SplitRootServerHostAndPort.class) + .addRoot("server", SplitRootServerName.class) + .build(); + + final ConfigMappings result = configMappingProvider.mapConfiguration(config); + final SplitRootServerHostAndPort server = result.getConfigMapping(SplitRootServerHostAndPort.class, "server"); + assertEquals("localhost", server.host()); + assertEquals(8080, server.port()); + + final SplitRootServerName name = result.getConfigMapping(SplitRootServerName.class, "server"); + assertEquals("konoha", name.name()); + } + + @Test + void splitRootsInConfig() { + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("server.host", "localhost", "server.port", "8080", "server.name", + "konoha")) + .withMapping(SplitRootServerHostAndPort.class, "server") + .withMapping(SplitRootServerName.class, "server") + .build(); + final SplitRootServerHostAndPort server = config.getConfigMapping(SplitRootServerHostAndPort.class, "server"); + assertEquals("localhost", server.host()); + assertEquals(8080, server.port()); + } + + @Test + void subGroups() { + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("server.host", "localhost", "server.port", "8080", "server.name", + "konoha")) + .withMapping(ServerSub.class, "server") + .build(); + final ServerSub server = config.getConfigMapping(ServerSub.class, "server"); + assertEquals("localhost", server.subHostAndPort().host()); + assertEquals(8080, server.subHostAndPort().port()); + assertEquals("konoha", server.subName().name()); + } + + @Test + void types() { + final Map typesConfig = new HashMap() { + { + put("int", "9"); + put("long", "9999999999"); + put("float", "99.9"); + put("double", "99.99"); + put("char", "c"); + put("boolean", "true"); + } + }; + + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config(typesConfig)) + .withMapping(SomeTypes.class) + .build(); + final SomeTypes types = config.getConfigMapping(SomeTypes.class); + + assertEquals(9, types.intPrimitive()); + assertEquals(9, types.intWrapper()); + assertEquals(9999999999L, types.longPrimitive()); + assertEquals(9999999999L, types.longWrapper()); + assertEquals(99.9f, types.floatPrimitive()); + assertEquals(99.9f, types.floatWrapper()); + assertEquals(99.99, types.doublePrimitive()); + assertEquals(99.99, types.doubleWrapper()); + assertEquals('c', types.charPrimitive()); + assertEquals('c', types.charWrapper()); + assertTrue(types.booleanPrimitive()); + assertTrue(types.booleanWrapper()); + } + + @Test + void optionals() { + final Map typesConfig = new HashMap() { + { + put("server.host", "localhost"); + put("server.port", "8080"); + put("optional", "optional"); + put("optional.int", "9"); + } + }; + + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config(typesConfig)) + .withMapping(Optionals.class) + .build(); + final Optionals optionals = config.getConfigMapping(Optionals.class); + + assertTrue(optionals.server().isPresent()); + assertEquals("localhost", optionals.server().get().host()); + assertEquals(8080, optionals.server().get().port()); + + assertTrue(optionals.optional().isPresent()); + assertEquals("optional", optionals.optional().get()); + assertTrue(optionals.optionalInt().isPresent()); + assertEquals(9, optionals.optionalInt().getAsInt()); + } + + @Test + void collectionTypes() { + final Map typesConfig = new HashMap() { + { + put("strings", "foo,bar"); + put("ints", "1,2,3"); + } + }; + + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config(typesConfig)) + .withMapping(CollectionTypes.class) + .build(); + final CollectionTypes types = config.getConfigMapping(CollectionTypes.class); + + assertEquals(Stream.of("foo", "bar").collect(toList()), types.listStrings()); + assertEquals(Stream.of(1, 2, 3).collect(toList()), types.listInts()); + } + + @Test + void maps() { + final Map typesConfig = new HashMap() { + { + put("server.host", "localhost"); + put("server.port", "8080"); + put("another-server", "localhost"); + } + }; + + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config(typesConfig)) + .withMapping(Maps.class) + .build(); + final Maps maps = config.getConfigMapping(Maps.class); + + assertEquals("localhost", maps.server().get("server").host()); + assertEquals("localhost", maps.anotherServer().get("another-server")); + } + + @Test + void defaults() { + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Defaults.class) + .build(); + final Defaults defaults = config.getConfigMapping(Defaults.class); + + assertEquals("foo", defaults.foo()); + assertEquals("bar", defaults.bar()); + assertEquals("foo", config.getRawValue("foo")); + + final List propertyNames = stream(config.getPropertyNames().spliterator(), false).collect(toList()); + assertFalse(propertyNames.contains("foo")); + } + + @Test + void converters() { + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("foo", "notbar")) + .withMapping(Converters.class) + .withConverter(String.class, 100, new FooBarConverter()) + .build(); + final Converters converters = config.getConfigMapping(Converters.class); + + assertEquals("bar", converters.foo()); + assertEquals("bar", config.getValue("foo", String.class)); + } + + @Test + void mix() { + final Map typesConfig = new HashMap() { + { + put("server.host", "localhost"); + put("server.port", "8080"); + put("server.name", "server"); + put("client.host", "clienthost"); + put("client.port", "80"); + put("client.name", "client"); + } + }; + + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config(typesConfig)) + .withMapping(ComplexSample.class) + .build(); + + final ComplexSample sample = config.getConfigMapping(ComplexSample.class); + assertEquals("localhost", sample.server().subHostAndPort().host()); + assertEquals(8080, sample.server().subHostAndPort().port()); + assertTrue(sample.client().isPresent()); + assertEquals("clienthost", sample.client().get().subHostAndPort().host()); + assertEquals(80, sample.client().get().subHostAndPort().port()); + } + + @Test + void noDynamicValues() { + final SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Server.class, "server") + .withSources(config("server.host", "localhost", "server.port", "8080")) + .withSources(new MapBackedConfigSource("test", new HashMap<>(), Integer.MAX_VALUE) { + private int counter = 1; + + @Override + public String getValue(final String propertyName) { + return counter++ + ""; + } + }).build(); + + final Server server = config.getConfigMapping(Server.class, "server"); + + assertNotEquals(config.getRawValue("server.port"), config.getRawValue("server.port")); + assertEquals(server.port(), server.port()); + } + + interface Server { + String host(); + + int port(); + } + + interface Client { + String host(); + + int port(); + } + + interface SplitRootServerHostAndPort { + String host(); + + int port(); + } + + interface SplitRootServerName { + String name(); + } + + interface ServerSub { + @WithParentName + ServerSubHostAndPort subHostAndPort(); + + @WithParentName + ServerSubName subName(); + } + + interface ServerSubHostAndPort { + String host(); + + int port(); + } + + interface ServerSubName { + String name(); + } + + public interface SomeTypes { + @WithName("int") + int intPrimitive(); + + @WithName("int") + Integer intWrapper(); + + @WithName("long") + long longPrimitive(); + + @WithName("long") + Long longWrapper(); + + @WithName("float") + float floatPrimitive(); + + @WithName("float") + Float floatWrapper(); + + @WithName("double") + double doublePrimitive(); + + @WithName("double") + Double doubleWrapper(); + + @WithName("char") + char charPrimitive(); + + @WithName("char") + Character charWrapper(); + + @WithName("boolean") + boolean booleanPrimitive(); + + @WithName("boolean") + Boolean booleanWrapper(); + } + + public interface Optionals { + Optional server(); + + Optional optional(); + + @WithName("optional.int") + OptionalInt optionalInt(); + } + + public interface CollectionTypes { + @WithName("strings") + List listStrings(); + + @WithName("ints") + List listInts(); + } + + public interface Maps { + @WithParentName + Map server(); + + Map anotherServer(); + } + + public interface Defaults { + @WithDefault("foo") + String foo(); + + @WithDefault("bar") + String bar(); + } + + public interface ComplexSample { + ServerSub server(); + + Optional client(); + } + + public interface Converters { + @WithConverter(FooBarConverter.class) + String foo(); + } + + public static class FooBarConverter implements Converter { + @Override + public String convert(final String value) { + return "bar"; + } + } +} diff --git a/implementation/src/test/java/io/smallrye/config/KeyMapBackedConfigSourceTest.java b/implementation/src/test/java/io/smallrye/config/KeyMapBackedConfigSourceTest.java new file mode 100644 index 000000000..6ce693a26 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/KeyMapBackedConfigSourceTest.java @@ -0,0 +1,46 @@ +package io.smallrye.config; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.junit.jupiter.api.Test; + +class KeyMapBackedConfigSourceTest { + @Test + void getProperties() { + KeyMap map = new KeyMap<>(); + map.findOrAdd("root.foo").putRootValue("bar"); + map.findOrAdd("root.foo.bar").putRootValue("baz"); + map.findOrAdd("root.foo.bar.*").putRootValue("baz"); + map.findOrAdd("root.foo.bar.*.baz").putRootValue("anything"); + + ConfigSource source = getSource(map); + Map properties = source.getProperties(); + assertTrue(properties.isEmpty()); + } + + @Test + void getValue() { + KeyMap map = new KeyMap<>(); + map.findOrAdd("root.foo").putRootValue("bar"); + map.findOrAdd("root.foo.bar").putRootValue("baz"); + map.findOrAdd("root.foo.bar.*").putRootValue("baz"); + map.findOrAdd("root.foo.bar.*.baz").putRootValue("anything"); + + ConfigSource source = getSource(map); + assertEquals("bar", source.getValue("root.foo")); + assertEquals("baz", source.getValue("root.foo.bar")); + assertEquals("baz", source.getValue("root.foo.bar.x")); + assertEquals("baz", source.getValue("root.foo.bar.y")); + assertEquals("anything", source.getValue("root.foo.bar.x.baz")); + assertEquals("anything", source.getValue("root.foo.bar.y.baz")); + assertNull(source.getValue("root.bar")); + assertNull(source.getValue("root.foo.bar.y.baz.z")); + } + + private ConfigSource getSource(final KeyMap properties) { + return new KeyMapBackedConfigSource("test", 0, properties); + } +} diff --git a/implementation/src/test/java/io/smallrye/config/KeyMapTest.java b/implementation/src/test/java/io/smallrye/config/KeyMapTest.java new file mode 100644 index 000000000..0a93065c9 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/KeyMapTest.java @@ -0,0 +1,120 @@ +package io.smallrye.config; + +import static java.util.stream.Collectors.toList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +public class KeyMapTest { + @Test + void find() { + KeyMap root = new KeyMap<>(); + root.findOrAdd("root").findOrAdd("foo").putRootValue("foo"); + root.findOrAdd("root").findOrAdd("bar").putRootValue("bar"); + + assertEquals("foo", root.findRootValue("root.foo")); + assertEquals("bar", root.findRootValue("root.bar")); + } + + @Test + void findOrAddPath() { + KeyMap map = new KeyMap<>(); + map.findOrAdd("root.foo").putRootValue("bar"); + map.findOrAdd("root.foo.bar").putRootValue("baz"); + map.findOrAdd("root.foo.bar.*").putRootValue("baz"); + map.findOrAdd("root.foo.bar.*.baz").putRootValue("anything"); + + assertEquals("bar", map.findRootValue("root.foo")); + assertEquals("baz", map.findRootValue("root.foo.bar")); + assertEquals("baz", map.findRootValue("root.foo.bar.x")); + assertEquals("baz", map.findRootValue("root.foo.bar.y")); + assertEquals("anything", map.findRootValue("root.foo.bar.x.baz")); + assertEquals("anything", map.findRootValue("root.foo.bar.y.baz")); + assertNull(map.findRootValue("root.bar")); + assertNull(map.findRootValue("root.foo.bar.y.baz.z")); + } + + @Test + void findOrAddVarArgs() { + KeyMap map = new KeyMap<>(); + map.findOrAdd("root", "foo").putRootValue("bar"); + map.findOrAdd("root", "foo", "bar").putRootValue("baz"); + map.findOrAdd("root", "foo", "bar", "*").putRootValue("baz"); + map.findOrAdd("root", "foo", "bar", "*", "baz").putRootValue("anything"); + + assertEquals("bar", map.findRootValue("root.foo")); + assertEquals("baz", map.findRootValue("root.foo.bar")); + assertEquals("baz", map.findRootValue("root.foo.bar.x")); + assertEquals("baz", map.findRootValue("root.foo.bar.y")); + assertEquals("anything", map.findRootValue("root.foo.bar.x.baz")); + assertEquals("anything", map.findRootValue("root.foo.bar.y.baz")); + assertNull(map.findRootValue("root.bar")); + assertNull(map.findRootValue("root.foo.bar.y.baz.z")); + } + + @Test + void findOrAddIterator() { + KeyMap map = new KeyMap<>(); + map.findOrAdd(Stream.of("root", "foo").collect(toList())).putRootValue("bar"); + map.findOrAdd(Stream.of("root", "foo", "bar").collect(toList())).putRootValue("baz"); + map.findOrAdd(Stream.of("root", "foo", "bar", "*").collect(toList())).putRootValue("baz"); + map.findOrAdd(Stream.of("root", "foo", "bar", "*", "baz").collect(toList())).putRootValue("anything"); + + assertEquals("bar", map.findRootValue("root.foo")); + assertEquals("baz", map.findRootValue("root.foo.bar")); + assertEquals("baz", map.findRootValue("root.foo.bar.x")); + assertEquals("baz", map.findRootValue("root.foo.bar.y")); + assertEquals("anything", map.findRootValue("root.foo.bar.x.baz")); + assertEquals("anything", map.findRootValue("root.foo.bar.y.baz")); + assertNull(map.findRootValue("root.bar")); + assertNull(map.findRootValue("root.foo.bar.y.baz.z")); + } + + @Test + void merge() { + KeyMap map = new KeyMap<>(); + map.findOrAdd("root.foo").putRootValue("bar"); + map.findOrAdd("root.foo.bar").putRootValue("baz"); + Map flatMap = new HashMap<>(); + flatMap.put("root.foo", "foo"); + flatMap.put("root.foo.bar.*", "baz"); + flatMap.put("root.foo.bar.*.baz", "anything"); + + flatMap.forEach((key, value) -> map.findOrAdd(key).putRootValue(value)); + + assertEquals("foo", map.findRootValue("root.foo")); + assertEquals("baz", map.findRootValue("root.foo.bar")); + assertEquals("baz", map.findRootValue("root.foo.bar.x")); + assertEquals("baz", map.findRootValue("root.foo.bar.y")); + assertEquals("anything", map.findRootValue("root.foo.bar.x.baz")); + assertEquals("anything", map.findRootValue("root.foo.bar.y.baz")); + assertNull(map.findRootValue("root.bar")); + assertNull(map.findRootValue("root.foo.bar.y.baz.z")); + } + + @Test + void empty() { + KeyMap map = new KeyMap<>(); + map.findOrAdd("", "foo").putRootValue("bar"); + + assertEquals("bar", map.findRootValue(".foo")); + } + + @Test + void string() { + KeyMap map = new KeyMap<>(); + map.findOrAdd("root.foo").putRootValue("bar"); + map.findOrAdd("root.foo.bar").putRootValue("baz"); + map.findOrAdd("root.foo.bar.*").putRootValue("baz"); + map.findOrAdd("root.foo.bar.*.baz").putRootValue("anything"); + + assertEquals( + "KeyMap(no value) {root=>KeyMap(no value) {foo=>KeyMap(value=bar) {bar=>KeyMap(value=baz) {(any)=>KeyMap(value=baz) {baz=>KeyMap(value=anything) {}}}}}}", + map.toString()); + } +} diff --git a/implementation/src/test/java/io/smallrye/config/KeyValuesConfigSource.java b/implementation/src/test/java/io/smallrye/config/KeyValuesConfigSource.java index 546886f62..e498fba7f 100644 --- a/implementation/src/test/java/io/smallrye/config/KeyValuesConfigSource.java +++ b/implementation/src/test/java/io/smallrye/config/KeyValuesConfigSource.java @@ -45,6 +45,10 @@ public String getName() { return "KeyValuesConfigSource"; } + public static ConfigSource config(Map properties) { + return new KeyValuesConfigSource(properties); + } + public static ConfigSource config(String... keyValues) { if (keyValues.length % 2 != 0) { throw new IllegalArgumentException("keyValues array must be a multiple of 2"); diff --git a/implementation/src/test/java/io/smallrye/config/inject/ConfigMappingInjectionTest.java b/implementation/src/test/java/io/smallrye/config/inject/ConfigMappingInjectionTest.java new file mode 100644 index 000000000..c18cc6604 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/inject/ConfigMappingInjectionTest.java @@ -0,0 +1,32 @@ +package io.smallrye.config.inject; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import javax.inject.Inject; + +import org.jboss.weld.junit5.WeldInitiator; +import org.jboss.weld.junit5.WeldJunit5Extension; +import org.jboss.weld.junit5.WeldSetup; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import io.smallrye.config.inject.InjectionTestConfigFactory.Server; + +@ExtendWith(WeldJunit5Extension.class) +public class ConfigMappingInjectionTest extends InjectionTest { + @WeldSetup + public WeldInitiator weld = WeldInitiator.from(ConfigExtension.class, Server.class) + .inject(this) + .build(); + + @Inject + Server server; + + @Test + void configMapping() { + assertNotNull(server); + assertEquals("localhost", server.host()); + assertEquals(8080, server.port()); + } +} diff --git a/implementation/src/test/java/io/smallrye/config/inject/InjectionTestConfigFactory.java b/implementation/src/test/java/io/smallrye/config/inject/InjectionTestConfigFactory.java index dbac8b7b2..96b6c5d44 100644 --- a/implementation/src/test/java/io/smallrye/config/inject/InjectionTestConfigFactory.java +++ b/implementation/src/test/java/io/smallrye/config/inject/InjectionTestConfigFactory.java @@ -5,6 +5,7 @@ import org.eclipse.microprofile.config.spi.ConfigSource; +import io.smallrye.config.ConfigMapping; import io.smallrye.config.KeyValuesConfigSource; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigFactory; @@ -39,6 +40,17 @@ public String getName() { .withSources(KeyValuesConfigSource.config("optional.int.value", "1", "optional.long.value", "2", "optional.double.value", "3.3")) .withSecretKeys("secret") + .withMapping(Server.class, "server") + .withDefaultValue("server.host", "localhost") + .withDefaultValue("server.port", "8080") .build(); } + + @ConfigMapping("server") + // TODO - radcortez - Add validation that interface has to be public. + public interface Server { + String host(); + + int port(); + } } diff --git a/pom.xml b/pom.xml index 3b22e0d48..799f585c2 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,7 @@ http://smallrye.io + 7.0 1.4 1.2.0 @@ -116,6 +117,12 @@ + + org.ow2.asm + asm + ${version.asm} + + io.smallrye.common @@ -132,6 +139,11 @@ smallrye-common-constraint ${version.smallrye.common} + + io.smallrye.common + smallrye-common-classloader + ${version.smallrye.common} +