diff --git a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java
new file mode 100644
index 000000000..04be4d50f
--- /dev/null
+++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java
@@ -0,0 +1,276 @@
+package io.smallrye.config;
+
+import java.io.Serializable;
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.ToDoubleFunction;
+import java.util.function.ToIntFunction;
+import java.util.function.ToLongFunction;
+
+import org.eclipse.microprofile.config.spi.Converter;
+
+/**
+ * A builder which can produce instances of a configuration interface annotated with {@link ConfigMapping}.
+ *
+ * Objects which are produced by this API will contain values for every property found on the configuration
+ * interface or its supertypes.
+ * If no value is given for a property, its default value is used.
+ * If a required property has no default value, then an exception will be thrown when {@link #build} is called.
+ * The returned object instance is immutable and has a stable {@code equals} and {@code hashCode} method.
+ * If the runtime is Java 16 or later, the returned object may be a {@code Record}.
+ *
+ * To provide a value for a property, use a method reference to indicate which property the value should be associated
+ * with. For example,
+ *
+ *
+
+
+ @ConfigMapping
+ interface MyProgramConfig {
+ String message();
+ int repeatCount();
+ }
+
+ ConfigInstanceBuilder<MyProgramConfig> builder = ConfigInstanceBuilder.forInterface(MyProgramConfig.class);
+ builder.with(MyProgramConfig::message, "Hello everyone!");
+ builder.with(MyProgramConfig::repeatCount, 42);
+
+ MyProgramConfig config = builder.build();
+ for (int i = 0; i < config.repeatCount(); i ++) {
+ System.out.println(config.message());
+ }
+
+ *
+ *
+ * Configuration interface member types are automatically converted with a {@link Converter}. Global converters are
+ * registered either by being discovered via the {@link java.util.ServiceLoader} mechanism, and can be
+ * registered by providing a {@code META-INF/services/org.eclipse.microprofile.config.spi.Converter} file, which
+ * contains the fully qualified class name of the custom {@code Converter} implementation, or explicitly by calling
+ * {@link ConfigInstanceBuilder#registerConverter(Class, Converter)}.
+ *
+ * Converters follow the same rules applied to {@link io.smallrye.config.SmallRyeConfig} and
+ * {@link io.smallrye.config.ConfigMapping}, including overriding the converter to use with
+ * {@link io.smallrye.config.WithConverter}.
+ *
+ * @param the configuration interface type
+ *
+ * @see io.smallrye.config.ConfigMapping
+ * @see org.eclipse.microprofile.config.spi.Converter
+ */
+public interface ConfigInstanceBuilder {
+ /**
+ * {@return the configuration interface (not null)}
+ */
+ Class configurationInterface();
+
+ /**
+ * Set a property on the configuration object to an object value.
+ *
+ * @param getter the property accessor (must not be {@code null})
+ * @param value the value to set (must not be {@code null})
+ * @return this builder (not {@code null})
+ * @param the value type
+ * @param the accessor type
+ * @throws IllegalArgumentException if the getter is {@code null}
+ * or if the value is {@code null}
+ */
+ & Serializable> ConfigInstanceBuilder with(F getter, T value);
+
+ /**
+ * Set a property on the configuration object to an integer value.
+ *
+ * @param getter the property accessor (must not be {@code null})
+ * @param value the value to set (must not be {@code null})
+ * @return this builder (not {@code null})
+ * @throws IllegalArgumentException if the getter is {@code null}
+ */
+ ConfigInstanceBuilder with(ToIntFunctionGetter getter, int value);
+
+ /**
+ * Set a property on the configuration object to a long value.
+ *
+ * @param getter the property accessor (must not be {@code null})
+ * @param value the value to set (must not be {@code null})
+ * @return this builder (not {@code null})
+ * @throws IllegalArgumentException if the getter is {@code null}
+ */
+ ConfigInstanceBuilder with(ToLongFunctionGetter getter, long value);
+
+ /**
+ * Set a property on the configuration object to a floating-point value.
+ *
+ * @param getter the property accessor (must not be {@code null})
+ * @param value the value to set (must not be {@code null})
+ * @return this builder (not {@code null})
+ * @throws IllegalArgumentException if the getter is {@code null}
+ */
+ ConfigInstanceBuilder with(ToDoubleFunctionGetter getter, double value);
+
+ /**
+ * Set a property on the configuration object to a boolean value.
+ *
+ * @param getter the property accessor (must not be {@code null})
+ * @param value the value to set (must not be {@code null})
+ * @return this builder (not {@code null})
+ * @param the accessor type
+ * @throws IllegalArgumentException if the getter is {@code null}
+ */
+ & Serializable> ConfigInstanceBuilder with(F getter, boolean value);
+
+ /**
+ * Set an optional property on the configuration object to an object value.
+ *
+ * @param getter the property accessor (must not be {@code null})
+ * @param value the value to set (must not be {@code null})
+ * @param the value type
+ * @param the accessor type
+ * @return this builder (not {@code null})
+ * @throws IllegalArgumentException if the getter is {@code null}
+ * or the value is {@code null}
+ */
+ default > & Serializable> ConfigInstanceBuilder withOptional(F getter,
+ T value) {
+ return with(getter, Optional.of(value));
+ }
+
+ /**
+ * Set an optional property on the configuration object to an integer value.
+ *
+ * @param getter the property accessor (must not be {@code null})
+ * @param value the value to set (must not be {@code null})
+ * @return this builder (not {@code null})
+ * @throws IllegalArgumentException if the getter is {@code null}
+ */
+ default ConfigInstanceBuilder withOptional(OptionalIntGetter getter, int value) {
+ return with(getter, OptionalInt.of(value));
+ }
+
+ /**
+ * Set an optional property on the configuration object to an integer value.
+ *
+ * @param getter the property accessor (must not be {@code null})
+ * @param value the value to set (must not be {@code null})
+ * @return this builder (not {@code null})
+ * @throws IllegalArgumentException if the getter is {@code null}
+ */
+ default ConfigInstanceBuilder withOptional(OptionalLongGetter getter, long value) {
+ return with(getter, OptionalLong.of(value));
+ }
+
+ /**
+ * Set an optional property on the configuration object to a floating-point value.
+ *
+ * @param getter the property accessor (must not be {@code null})
+ * @param value the value to set (must not be {@code null})
+ * @return this builder (not {@code null})
+ * @throws IllegalArgumentException if the getter is {@code null}
+ */
+ default ConfigInstanceBuilder withOptional(OptionalDoubleGetter getter, double value) {
+ return with(getter, OptionalDouble.of(value));
+ }
+
+ /**
+ * Set an optional property on the configuration object to a boolean value.
+ *
+ * @param getter the property accessor (must not be {@code null})
+ * @param value the value to set (must not be {@code null})
+ * @param the accessor type
+ * @return this builder (not {@code null})
+ * @throws IllegalArgumentException if the getter is {@code null}
+ */
+ default > & Serializable> ConfigInstanceBuilder withOptional(F getter,
+ boolean value) {
+ return with(getter, Optional.of(value));
+ }
+
+ /**
+ * Build the configuration instance.
+ *
+ * @return the configuration instance (not {@code null})
+ * @throws IllegalArgumentException if a required property does not have a value
+ */
+ I build();
+
+ /**
+ * Get a builder instance for the given configuration interface.
+ *
+ * @param interfaceClass the interface class object (must not be {@code null})
+ * @param the configuration interface type
+ * @return a new builder for the configuration interface (not {@code null})
+ * @throws IllegalArgumentException if the interface class is {@code null},
+ * or if the class object does not represent an interface,
+ * or if the interface is not a valid configuration interface,
+ * or if the interface has one or more required properties that were not given a value,
+ * or if the interface has one or more converters that could not be instantiated
+ * @throws SecurityException if this class does not have permission to introspect the given interface
+ * or one of its superinterfaces
+ */
+ static ConfigInstanceBuilder forInterface(Class interfaceClass)
+ throws IllegalArgumentException, SecurityException {
+ return ConfigInstanceBuilderImpl.forInterface(interfaceClass);
+ }
+
+ /**
+ * Globally registers a {@link org.eclipse.microprofile.config.spi.Converter} to be used by the
+ * {@link io.smallrye.config.ConfigInstanceBuilder} to convert configuration interface member types.
+ *
+ * @param type the class of the type to convert
+ * @param converter the converter instance that can convert to the type
+ * @param the type to convert
+ */
+ static void registerConverter(Class type, Converter converter) {
+ ConfigInstanceBuilderImpl.CONVERTERS.put(type, converter);
+ }
+
+ /**
+ * Represents a getter in the configuration interface of primitive type {@code int}.
+ *
+ * @param the configuration interface type
+ */
+ interface ToIntFunctionGetter extends ToIntFunction, Serializable {
+ }
+
+ /**
+ * Represents a getter in the configuration interface of primitive type {@code long}.
+ *
+ * @param the configuration interface type
+ */
+ interface ToLongFunctionGetter extends ToLongFunction, Serializable {
+ }
+
+ /**
+ * Represents a getter in the configuration interface of primitive type {@code double}.
+ *
+ * @param the configuration interface type
+ */
+ interface ToDoubleFunctionGetter extends ToDoubleFunction, Serializable {
+ }
+
+ /**
+ * Represents a getter in the configuration interface of type {@code OptionalInt}.
+ *
+ * @param the configuration interface type
+ */
+ interface OptionalIntGetter extends Function, Serializable {
+ }
+
+ /**
+ * Represents a getter in the configuration interface of type {@code OptionalLong}.
+ *
+ * @param the configuration interface type
+ */
+ interface OptionalLongGetter extends Function, Serializable {
+ }
+
+ /**
+ * Represents a getter in the configuration interface of type {@code OptionalDouble}.
+ *
+ * @param the configuration interface type
+ */
+ interface OptionalDoubleGetter extends Function, Serializable {
+ }
+}
diff --git a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java
new file mode 100644
index 000000000..3b9f2423c
--- /dev/null
+++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java
@@ -0,0 +1,439 @@
+package io.smallrye.config;
+
+import static io.smallrye.config.Converters.newCollectionConverter;
+import static io.smallrye.config.Converters.newOptionalConverter;
+import static io.smallrye.config._private.ConfigMessages.msg;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.invoke.SerializedLambda;
+import java.lang.reflect.Type;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.IntFunction;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+import org.eclipse.microprofile.config.spi.Converter;
+
+import io.smallrye.common.constraint.Assert;
+import io.smallrye.config.Converters.Implicit;
+import io.smallrye.config.SmallRyeConfigBuilder.ConverterWithPriority;
+import io.smallrye.config._private.ConfigMessages;
+import sun.reflect.ReflectionFactory;
+
+/**
+ * The implementation for configuration instance builders.
+ */
+final class ConfigInstanceBuilderImpl implements ConfigInstanceBuilder {
+
+ /**
+ * Reflection factory, used for getting the serialized lambda information out of a getter reference.
+ */
+ private static final ReflectionFactory rf = ReflectionFactory.getReflectionFactory();
+ /**
+ * Stack walker for getting caller class, used for setter caching.
+ */
+ private static final StackWalker sw = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
+ /**
+ * Our cached lookup object.
+ */
+ private static final MethodHandles.Lookup myLookup = MethodHandles.lookup();
+ /**
+ * Class value which holds the cached builder class instance.
+ */
+ private static final ClassValue> builderFactories = new ClassValue<>() {
+ protected Supplier> computeValue(final Class> type) {
+ assert type.isInterface();
+ // TODO - Should we cache this eagerly in io.smallrye.config.ConfigMappingLoader.ConfigMappingImplementation?
+ MethodHandles.Lookup lookup;
+ try {
+ lookup = MethodHandles.privateLookupIn(type, myLookup);
+ } catch (IllegalAccessException e) {
+ throw msg.accessDenied(getClass(), type);
+ }
+ Class> impl;
+ try {
+ ConfigMappingLoader.ensureLoaded(type);
+ impl = lookup.findClass(ConfigMappingInterface.ConfigMappingBuilder.getBuilderClassName(type));
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException(e);
+ } catch (IllegalAccessException e) {
+ throw msg.accessDenied(getClass(), type);
+ }
+ MethodHandle mh;
+ try {
+ mh = lookup.findConstructor(impl, MethodType.methodType(void.class));
+ } catch (NoSuchMethodException e) {
+ throw msg.noConstructor(impl);
+ } catch (IllegalAccessException e) {
+ throw msg.accessDenied(getClass(), impl);
+ }
+ // capture the constructor as a Supplier
+ return () -> {
+ try {
+ return mh.invoke();
+ } catch (RuntimeException | Error e) {
+ throw e;
+ } catch (Throwable e) {
+ throw new UndeclaredThrowableException(e);
+ }
+ };
+ }
+ };
+
+ /**
+ * Class value which holds the cached config class instance constructors.
+ */
+ private static final ClassValue> configFactories = new ClassValue<>() {
+ // TODO - This is to load the mapping class implementation, which we already have, just missing the right constructor in the ConfigMappingLoader, so we can probably remove this one
+ protected Function computeValue(final Class> type) {
+ assert type.isInterface();
+ MethodHandles.Lookup lookup;
+ try {
+ lookup = MethodHandles.privateLookupIn(type, myLookup);
+ } catch (IllegalAccessException e) {
+ throw msg.accessDenied(getClass(), type);
+ }
+ Class> impl;
+ Class> builderClass;
+ try {
+ impl = ConfigMappingLoader.ensureLoaded(type).implementation();
+ builderClass = lookup.findClass(ConfigMappingInterface.ConfigMappingBuilder.getBuilderClassName(type));
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException(e);
+ } catch (IllegalAccessException e) {
+ throw msg.accessDenied(getClass(), type);
+ }
+ MethodHandle mh;
+
+ try {
+ mh = lookup.findConstructor(impl, MethodType.methodType(void.class, builderClass));
+ } catch (NoSuchMethodException e) {
+ throw msg.noConstructor(impl);
+ } catch (IllegalAccessException e) {
+ throw msg.accessDenied(getClass(), impl);
+ }
+ // capture the constructor as a Function
+ return builder -> {
+ try {
+ return mh.invoke(builder);
+ } catch (RuntimeException | Error e) {
+ throw e;
+ } catch (Throwable e) {
+ throw new UndeclaredThrowableException(e);
+ }
+ };
+ }
+ };
+
+ /**
+ * Class value that holds the cache of maps of method reference lambdas to their corresponding setter.
+ */
+ private static final ClassValue>> setterMapsByCallingClass = new ClassValue<>() {
+ protected Map> computeValue(final Class> type) {
+ return new ConcurrentHashMap<>();
+ }
+ };
+
+ // =====================================
+
+ static ConfigInstanceBuilderImpl forInterface(Class configurationInterface)
+ throws IllegalArgumentException, SecurityException {
+ return new ConfigInstanceBuilderImpl<>(configurationInterface, builderFactories.get(configurationInterface).get());
+ }
+
+ // =====================================
+
+ private final Class configurationInterface;
+ private final MethodHandles.Lookup lookup;
+ private final Object builderObject;
+
+ ConfigInstanceBuilderImpl(final Class configurationInterface, final Object builderObject) {
+ this.configurationInterface = configurationInterface;
+ try {
+ lookup = MethodHandles.privateLookupIn(builderObject.getClass(), myLookup);
+ } catch (IllegalAccessException e) {
+ throw msg.accessDenied(builderObject.getClass(), getClass());
+ }
+ this.builderObject = builderObject;
+ }
+
+ // =====================================
+
+ public Class configurationInterface() {
+ return configurationInterface;
+ }
+
+ // -------------------------------------
+
+ public & Serializable> ConfigInstanceBuilder with(final F getter, final T value) {
+ Assert.checkNotNullParam("getter", getter);
+ Assert.checkNotNullParam("value", value);
+ Class> callerClass = sw.getCallerClass();
+ BiConsumer setter = getSetter(getter, callerClass);
+ setter.accept(builderObject, value);
+ return this;
+ }
+
+ public ConfigInstanceBuilder with(final ToIntFunctionGetter getter, final int value) {
+ Assert.checkNotNullParam("getter", getter);
+ Class> callerClass = sw.getCallerClass();
+ BiConsumer setter = getSetter(getter, callerClass);
+ setter.accept(builderObject, value);
+ return this;
+ }
+
+ public ConfigInstanceBuilder with(final ToLongFunctionGetter getter, final long value) {
+ Assert.checkNotNullParam("getter", getter);
+ Class> callerClass = sw.getCallerClass();
+ BiConsumer setter = getSetter(getter, callerClass);
+ setter.accept(builderObject, value);
+ return this;
+ }
+
+ public ConfigInstanceBuilder with(final ToDoubleFunctionGetter getter, final double value) {
+ Assert.checkNotNullParam("getter", getter);
+ Class> callerClass = sw.getCallerClass();
+ BiConsumer setter = getSetter(getter, callerClass);
+ setter.accept(builderObject, value);
+ return this;
+ }
+
+ public & Serializable> ConfigInstanceBuilder with(final F getter, final boolean value) {
+ Assert.checkNotNullParam("getter", getter);
+ Class> callerClass = sw.getCallerClass();
+ BiConsumer setter = getSetter(getter, callerClass);
+ setter.accept(builderObject, value);
+ return this;
+ }
+
+ public I build() {
+ return configurationInterface.cast(configFactories.get(configurationInterface).apply(builderObject));
+ }
+
+ // =====================================
+
+ static final Map> CONVERTERS = new ConcurrentHashMap<>();
+
+ static {
+ registerConverters();
+ }
+
+ private static void registerConverters() {
+ Map convertersToBuild = new HashMap<>();
+
+ // TODO - We need to register this for Native in Quarkus - Also, we are doubling the work because SR Config also does the registration
+ for (Converter> converter : ServiceLoader.load(Converter.class, SecuritySupport.getContextClassLoader())) {
+ Type type = Converters.getConverterType(converter.getClass());
+ if (type == null) {
+ throw ConfigMessages.msg.unableToAddConverter(converter);
+ }
+ SmallRyeConfigBuilder.addConverter(type, converter, convertersToBuild);
+ }
+
+ CONVERTERS.putAll(Converters.ALL_CONVERTERS);
+ CONVERTERS.put(ConfigValue.class, Converters.CONFIG_VALUE_CONVERTER);
+ for (Entry entry : convertersToBuild.entrySet()) {
+ CONVERTERS.put(entry.getKey(), entry.getValue().getConverter());
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Converter getConverter(Class type) {
+ Converter> exactConverter = CONVERTERS.get(type);
+ if (exactConverter != null) {
+ return (Converter) exactConverter;
+ }
+ if (type.isPrimitive()) {
+ return (Converter) getConverter(Converters.wrapPrimitiveType(type));
+ }
+ if (type.isArray()) {
+ Converter> conv = getConverter(type.getComponentType());
+ if (conv != null) {
+ return Converters.newArrayConverter(conv, type);
+ }
+ throw ConfigMessages.msg.noRegisteredConverter(type);
+ }
+
+ Converter converter = Implicit.getConverter(type);
+ if (converter == null) {
+ throw ConfigMessages.msg.noRegisteredConverter(type);
+ }
+ return converter;
+ }
+
+ public static T convertValue(final String value, final Converter converter) {
+ T convert = converter.convert(value);
+ if (convert == null) {
+ // TODO - new messsage instead of reuse?
+ throw ConfigMessages.msg.converterReturnedNull("", value, converter.getClass().getTypeName());
+ }
+ return convert;
+ }
+
+ @SuppressWarnings("unused")
+ public static Optional convertOptionalValue(final String value, final Converter converter) {
+ return convertValue(value, Converters.newOptionalConverter(converter));
+ }
+
+ @SuppressWarnings({ "unchecked", "unused" })
+ public static > C convertValues(
+ final String value,
+ final Converter converter,
+ final Class collectionType) {
+ return (C) convertValue(value, newCollectionConverter(converter, createCollectionFactory(collectionType)));
+ }
+
+ @SuppressWarnings({ "unchecked", "unused" })
+ public static > Optional convertOptionalValues(
+ final String value,
+ final Converter converter,
+ final Class collectionType) {
+ Converter> collectionConverter = newCollectionConverter(converter,
+ createCollectionFactory(collectionType));
+ return (Optional) newOptionalConverter(collectionConverter).convert(value);
+ }
+
+ @SuppressWarnings("unused")
+ public static T requireValue(final T value, final String name) {
+ if (value == null) {
+ throw msg.propertyNotSet(name);
+ }
+ return value;
+ }
+
+ public static > IntFunction extends Collection> createCollectionFactory(
+ final Class type) {
+ if (type.equals(List.class)) {
+ return ArrayList::new;
+ }
+
+ if (type.equals(Set.class)) {
+ return HashSet::new;
+ }
+
+ throw new IllegalArgumentException();
+ }
+
+ public static class MapWithDefault extends HashMap {
+ @Serial
+ private static final long serialVersionUID = 1390928078837140814L;
+ private final V defaultValue;
+
+ MapWithDefault(final V defaultValue) {
+ this.defaultValue = defaultValue;
+ }
+
+ @Override
+ public V get(final Object key) {
+ return getOrDefault(key, defaultValue);
+ }
+ }
+
+ private BiConsumer getSetter(final Object getter, final Class> callerClass) {
+ Map> setterMap = setterMapsByCallingClass.get(callerClass);
+ BiConsumer setter = setterMap.get(getter);
+ if (setter == null) {
+ setter = setterMap.computeIfAbsent(getter, this::createSetter);
+ }
+ return setter;
+ }
+
+ private BiConsumer createSetter(Object lambda) {
+ MethodHandle writeReplace = rf.writeReplaceForSerialization(lambda.getClass());
+ if (writeReplace == null) {
+ throw msg.invalidGetter();
+ }
+ Object replaced;
+ try {
+ replaced = writeReplace.invoke(lambda);
+ } catch (RuntimeException | Error e) {
+ throw e;
+ } catch (Throwable e) {
+ throw new UndeclaredThrowableException(e);
+ }
+ if (!(replaced instanceof SerializedLambda sl)) {
+ throw msg.invalidGetter();
+ }
+ if (sl.getCapturedArgCount() != 0) {
+ throw msg.invalidGetter();
+ }
+ // TODO: check implClassName against the supertype hierarchy of the config interface using shared info mapping
+ String setterName = sl.getImplMethodName();
+ Class> type = parseReturnType(sl.getImplMethodSignature());
+ return createSetterByName(setterName, type);
+ }
+
+ private BiConsumer createSetterByName(final String setterName, final Class> type) {
+ Class> builderClass = builderObject.getClass();
+ MethodHandle setter;
+ try {
+ setter = lookup.findVirtual(builderClass, setterName, MethodType.methodType(void.class, type));
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ } catch (IllegalAccessException e) {
+ throw msg.accessDenied(getClass(), builderClass);
+ }
+ // adapt it to be an object consumer
+ MethodHandle castSetter = setter.asType(MethodType.methodType(void.class, builderClass, Object.class));
+ return (builder, val) -> {
+ try {
+ castSetter.invoke(builderObject, val);
+ } catch (RuntimeException | Error e) {
+ throw e;
+ } catch (Throwable e) {
+ throw new UndeclaredThrowableException(e);
+ }
+ };
+ }
+
+ private Class> parseReturnType(final String signature) {
+ int idx = signature.lastIndexOf(')');
+ if (idx == -1) {
+ throw new IllegalStateException("Unexpected invalid signature");
+ }
+ return parseType(signature, idx + 1, signature.length());
+ }
+
+ private Class> parseType(String desc, int start, int end) {
+ return switch (desc.charAt(start)) {
+ case 'L' -> parseClassName(desc, start + 1, end - 1);
+ case '[' -> parseType(desc, start + 1, end).arrayType();
+ case 'B' -> byte.class;
+ case 'C' -> char.class;
+ case 'D' -> double.class;
+ case 'F' -> float.class;
+ case 'I' -> int.class;
+ case 'J' -> long.class;
+ case 'S' -> short.class;
+ case 'Z' -> boolean.class;
+ default -> throw msg.invalidGetter();
+ };
+ }
+
+ private Class> parseClassName(final String signature, final int start, final int end) {
+ try {
+ return lookup.findClass(signature.substring(start, end).replaceAll("/", "."));
+ } catch (ClassNotFoundException e) {
+ throw msg.invalidGetter();
+ } catch (IllegalAccessException e) {
+ throw msg.accessDenied(getClass(), builderObject.getClass());
+ }
+ }
+}
diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java
index 3f4510ca6..b046d4039 100644
--- a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java
+++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java
@@ -1,12 +1,12 @@
package io.smallrye.config;
+import static io.smallrye.config.ConfigInstanceBuilderImpl.createCollectionFactory;
import static io.smallrye.config.ConfigMappingLoader.configMappingProperties;
import static io.smallrye.config.ConfigMappingLoader.getConfigMappingClass;
import static io.smallrye.config.ConfigValidationException.Problem;
import static io.smallrye.config.Converters.newSecretConverter;
import static io.smallrye.config.common.utils.StringUtil.unindexed;
-import java.io.Serial;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
@@ -27,6 +27,7 @@
import org.eclipse.microprofile.config.spi.Converter;
+import io.smallrye.config.ConfigInstanceBuilderImpl.MapWithDefault;
import io.smallrye.config.ConfigMapping.NamingStrategy;
import io.smallrye.config.ConfigMappings.ConfigClass;
import io.smallrye.config.SmallRyeConfigBuilder.MappingBuilder;
@@ -1056,19 +1057,4 @@ private static String quoted(final String key) {
return keyIterator.hasNext() ? "\"" + key + "\"" : key;
}
}
-
- static class MapWithDefault extends HashMap {
- @Serial
- private static final long serialVersionUID = 1390928078837140814L;
- private final V defaultValue;
-
- MapWithDefault(final V defaultValue) {
- this.defaultValue = defaultValue;
- }
-
- @Override
- public V get(final Object key) {
- return getOrDefault(key, defaultValue);
- }
- }
}
diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java
index 4984fe9dd..92f4f3497 100644
--- a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java
+++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java
@@ -1,5 +1,6 @@
package io.smallrye.config;
+import static io.smallrye.config.ConfigMappingInterface.ConfigMappingBuilder.getBuilderClassName;
import static org.objectweb.asm.Opcodes.AASTORE;
import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
@@ -11,14 +12,15 @@
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ANEWARRAY;
import static org.objectweb.asm.Opcodes.ARETURN;
-import static org.objectweb.asm.Opcodes.ASM7;
import static org.objectweb.asm.Opcodes.ASTORE;
import static org.objectweb.asm.Opcodes.BIPUSH;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.DCMPL;
+import static org.objectweb.asm.Opcodes.DLOAD;
import static org.objectweb.asm.Opcodes.DRETURN;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.FCMPL;
+import static org.objectweb.asm.Opcodes.FLOAD;
import static org.objectweb.asm.Opcodes.FRETURN;
import static org.objectweb.asm.Opcodes.F_SAME;
import static org.objectweb.asm.Opcodes.GETFIELD;
@@ -32,12 +34,14 @@
import static org.objectweb.asm.Opcodes.IF_ACMPEQ;
import static org.objectweb.asm.Opcodes.IF_ACMPNE;
import static org.objectweb.asm.Opcodes.IF_ICMPNE;
+import static org.objectweb.asm.Opcodes.ILOAD;
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.IRETURN;
import static org.objectweb.asm.Opcodes.LCMP;
+import static org.objectweb.asm.Opcodes.LLOAD;
import static org.objectweb.asm.Opcodes.LRETURN;
import static org.objectweb.asm.Opcodes.NEW;
import static org.objectweb.asm.Opcodes.POP;
@@ -55,21 +59,22 @@
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
import java.util.regex.Pattern;
import org.eclipse.microprofile.config.inject.ConfigProperties;
import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipse.microprofile.config.spi.Converter;
import org.objectweb.asm.AnnotationVisitor;
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;
@@ -78,6 +83,7 @@
import io.smallrye.config.ConfigMapping.NamingStrategy;
import io.smallrye.config.ConfigMappingContext.ObjectCreator;
import io.smallrye.config.ConfigMappingInterface.CollectionProperty;
+import io.smallrye.config.ConfigMappingInterface.GroupProperty;
import io.smallrye.config.ConfigMappingInterface.LeafProperty;
import io.smallrye.config.ConfigMappingInterface.MapProperty;
import io.smallrye.config.ConfigMappingInterface.MayBeOptionalProperty;
@@ -85,17 +91,11 @@
import io.smallrye.config.ConfigMappingInterface.Property;
public class ConfigMappingGenerator {
- static final boolean usefulDebugInfo;
/**
* The regular expression allowing to detect arrays in a full type name.
*/
private static final Pattern ARRAY_FORMAT_REGEX = Pattern.compile("([<;])L(.*)\\[];");
- static {
- usefulDebugInfo = Boolean.parseBoolean(AccessController.doPrivileged(
- (PrivilegedAction) () -> System.getProperty("io.smallrye.config.mapper.useful-debug-info")));
- }
-
private static final String I_CLASS = getInternalName(Class.class);
private static final String I_FIELD = getInternalName(Field.class);
@@ -107,7 +107,10 @@ public class ConfigMappingGenerator {
private static final String I_RUNTIME_EXCEPTION = getInternalName(RuntimeException.class);
private static final String I_OBJECT = getInternalName(Object.class);
private static final String I_STRING = getInternalName(String.class);
+ private static final String I_OPTIONAL = getInternalName(Optional.class);
private static final String I_ITERABLE = getInternalName(Iterable.class);
+ private static final String I_COLLECTION = getInternalName(Collection.class);
+ private static final String I_MAP = getInternalName(Map.class);
private static final int V_THIS = 0;
private static final int V_MAPPING_CONTEXT = 1;
@@ -119,9 +122,7 @@ public class ConfigMappingGenerator {
* @return the class bytes representing the implementation of the configuration interface.
*/
static byte[] generate(final ConfigMappingInterface mapping) {
- ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
- ClassVisitor visitor = usefulDebugInfo ? new Debugging.ClassVisitorImpl(writer) : writer;
-
+ ClassWriter visitor = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
visitor.visit(V1_8, ACC_PUBLIC, mapping.getClassInternalName(), null, I_OBJECT,
new String[] { getInternalName(mapping.getInterfaceType()) });
visitor.visitSource(null, null);
@@ -134,6 +135,50 @@ static byte[] generate(final ConfigMappingInterface mapping) {
noArgsCtor.visitEnd();
noArgsCtor.visitMaxs(0, 0);
+ // Builder Constructor
+ String builderName = getBuilderClassName(mapping.getInterfaceType()).replace('.', '/');
+ MethodVisitor builderCtor = visitor.visitMethod(ACC_PUBLIC, "", "(L" + builderName + ";)V", null, null);
+ builderCtor.visitVarInsn(ALOAD, V_THIS);
+ builderCtor.visitMethodInsn(INVOKESPECIAL, I_OBJECT, "", "()V", false);
+ for (Property property : mapping.getProperties()) {
+ if (!property.isDefaultMethod()) {
+ Method method = property.getMethod();
+ String memberName = method.getName();
+ String fieldDesc = getDescriptor(method.getReturnType());
+ builderCtor.visitVarInsn(ALOAD, V_THIS);
+ builderCtor.visitVarInsn(ALOAD, 1);
+ builderCtor.visitMethodInsn(INVOKEVIRTUAL, builderName, memberName, "()" + fieldDesc, false);
+ if (!property.isPrimitive()) {
+ builderCtor.visitLdcInsn(method.getDeclaringClass().getName() + "." + memberName);
+ builderCtor.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "requireValue",
+ "(L" + I_OBJECT + ";L" + I_STRING + ";)L" + I_OBJECT + ";", false);
+ builderCtor.visitTypeInsn(CHECKCAST, getInternalName(method.getReturnType()));
+ }
+ builderCtor.visitFieldInsn(PUTFIELD, mapping.getClassInternalName(), memberName, fieldDesc);
+ }
+ }
+ // We don't know the order in the constructor and the default method may require call to other
+ // properties that may not be initialized yet, so we add them last
+ for (Property property : mapping.getProperties()) {
+ Method method = property.getMethod();
+ String memberName = method.getName();
+ String fieldDesc = getDescriptor(method.getReturnType());
+
+ if (property.isDefaultMethod()) {
+ builderCtor.visitVarInsn(ALOAD, V_THIS);
+ Method defaultMethod = property.asDefaultMethod().getDefaultMethod();
+ builderCtor.visitVarInsn(ALOAD, V_THIS);
+ builderCtor.visitMethodInsn(INVOKESTATIC, getInternalName(defaultMethod.getDeclaringClass()),
+ defaultMethod.getName(),
+ "(" + getType(mapping.getInterfaceType()) + ")" + fieldDesc, false);
+ builderCtor.visitFieldInsn(PUTFIELD, mapping.getClassInternalName(), memberName, fieldDesc);
+ }
+ }
+
+ builderCtor.visitInsn(RETURN);
+ builderCtor.visitEnd();
+ builderCtor.visitMaxs(0, 0);
+
ObjectCreatorMethodVisitor ctor = new ObjectCreatorMethodVisitor(
visitor.visitMethod(ACC_PUBLIC, "", "(L" + I_MAPPING_CONTEXT + ";)V", null, null));
ctor.visitParameter("context", ACC_FINAL);
@@ -170,7 +215,379 @@ static byte[] generate(final ConfigMappingInterface mapping) {
generateHashCode(visitor, mapping);
generateToString(visitor, mapping);
- return writer.toByteArray();
+ return visitor.toByteArray();
+ }
+
+ private static final String I_CONFIG_INSTANCE_BUILDER = getInternalName(ConfigInstanceBuilder.class);
+ private static final String I_CONFIG_INSTANCE_BUILDER_IMPL = getInternalName(ConfigInstanceBuilderImpl.class);
+ private static final String I_OPTIONAL_INT = getInternalName(OptionalInt.class);
+ private static final String I_OPTIONAL_LONG = getInternalName(OptionalLong.class);
+ private static final String I_OPTIONAL_DOUBLE = getInternalName(OptionalDouble.class);
+ private static final String I_CONVERTER = getInternalName(Converter.class);
+
+ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String className) {
+ ClassWriter visitor = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
+ visitor.visit(V1_8, ACC_PUBLIC, className, null, I_OBJECT, new String[] {});
+ visitor.visitSource(null, null);
+
+ // No Args Constructor
+ MethodVisitor ctor = visitor.visitMethod(ACC_PUBLIC, "", "()V", null, null);
+ ctor.visitVarInsn(ALOAD, V_THIS);
+ ctor.visitMethodInsn(INVOKESPECIAL, I_OBJECT, "", "()V", false);
+ for (Property property : mapping.getProperties()) {
+ if (property.isDefaultMethod()) {
+ continue;
+ }
+
+ // Set Default / Generate method to retrieve the default
+ String fieldDesc = getDescriptor(property.getMethod().getReturnType());
+ String memberName = property.getMethod().getName();
+ String defaultMethodName = "default_" + memberName;
+ boolean generateGetterWithDefaullt = false;
+ if (property.isPrimitive() && property.hasDefaultValue() && property.getDefaultValue() != null) {
+ // Primitive inline default in field, since it cumbersome to test if it was set by the API
+ ctor.visitVarInsn(ALOAD, V_THIS);
+ ctor.visitMethodInsn(INVOKESTATIC, className, defaultMethodName, "()" + fieldDesc, false);
+ ctor.visitFieldInsn(PUTFIELD, className, memberName, fieldDesc);
+ // Default Method
+ PrimitiveProperty primitiveProperty = property.asPrimitive();
+ MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc, null,
+ null);
+ mv.visitLdcInsn(property.getDefaultValue());
+ if (primitiveProperty.hasConvertWith()) {
+ String convertWith = getInternalName(primitiveProperty.getConvertWith());
+ mv.visitTypeInsn(NEW, convertWith);
+ mv.visitInsn(DUP);
+ mv.visitMethodInsn(INVOKESPECIAL, convertWith, "", "()V", false);
+ } else {
+ mv.visitLdcInsn(Type.getType(getDescriptor(primitiveProperty.getBoxType())));
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "getConverter",
+ "(L" + I_CLASS + ";)L" + I_CONVERTER + ";", false);
+ }
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValue",
+ "(L" + I_STRING + ";L" + I_CONVERTER + ";)L" + I_OBJECT + ";", false);
+ mv.visitTypeInsn(CHECKCAST, getInternalName(primitiveProperty.getBoxType()));
+ mv.visitMethodInsn(INVOKEVIRTUAL,
+ getInternalName(primitiveProperty.getBoxType()),
+ primitiveProperty.getUnboxMethodName(),
+ primitiveProperty.getUnboxMethodDescriptor(), false);
+ mv.visitInsn(getReturnInstruction(property));
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ } else if (property.isLeaf() && !property.isOptional()) {
+ LeafProperty leafProperty = property.asLeaf();
+ if (property.hasDefaultValue() && property.getDefaultValue() != null) {
+ generateGetterWithDefaullt = true;
+ // Default Method
+ MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc, null,
+ null);
+ mv.visitLdcInsn(property.getDefaultValue());
+ if (leafProperty.hasConvertWith()) {
+ String convertWith = getInternalName(leafProperty.getConvertWith());
+ mv.visitTypeInsn(NEW, convertWith);
+ mv.visitInsn(DUP);
+ mv.visitMethodInsn(INVOKESPECIAL, convertWith, "", "()V", false);
+ } else {
+ mv.visitLdcInsn(Type.getType(fieldDesc));
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "getConverter",
+ "(L" + I_CLASS + ";)L" + I_CONVERTER + ";", false);
+ }
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValue",
+ "(L" + I_STRING + ";L" + I_CONVERTER + ";)L" + I_OBJECT + ";", false);
+ mv.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType()));
+ mv.visitInsn(getReturnInstruction(property));
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ } else {
+ // There is no default, but we initialize empty Optionals inline in field
+ if (leafProperty.getValueRawType().equals(OptionalInt.class)) {
+ ctor.visitVarInsn(ALOAD, V_THIS);
+ ctor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_INT, "empty", "()L" + I_OPTIONAL_INT + ";", false);
+ ctor.visitFieldInsn(PUTFIELD, className, memberName, "L" + I_OPTIONAL_INT + ";");
+ } else if (leafProperty.getValueRawType().equals(OptionalLong.class)) {
+ ctor.visitVarInsn(ALOAD, V_THIS);
+ ctor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_LONG, "empty", "()L" + I_OPTIONAL_LONG + ";",
+ false);
+ ctor.visitFieldInsn(PUTFIELD, className, memberName, "L" + I_OPTIONAL_LONG + ";");
+ } else if (leafProperty.getValueRawType().equals(OptionalDouble.class)) {
+ ctor.visitVarInsn(ALOAD, V_THIS);
+ ctor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_DOUBLE, "empty", "()L" + I_OPTIONAL_DOUBLE + ";",
+ false);
+ ctor.visitFieldInsn(PUTFIELD, className, memberName, "L" + I_OPTIONAL_DOUBLE + ";");
+ }
+ }
+ } else if (property.isOptional() && property.isLeaf()) {
+ if (property.hasDefaultValue() && property.getDefaultValue() != null) {
+ generateGetterWithDefaullt = true;
+ // Default Method
+ MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc, null,
+ null);
+ LeafProperty optionalProperty = property.asLeaf();
+ mv.visitLdcInsn(property.getDefaultValue());
+ if (optionalProperty.hasConvertWith()) {
+ String convertWith = getInternalName(optionalProperty.getConvertWith());
+ mv.visitTypeInsn(NEW, convertWith);
+ mv.visitInsn(DUP);
+ mv.visitMethodInsn(INVOKESPECIAL, convertWith, "", "()V", false);
+ } else {
+ mv.visitLdcInsn(Type.getType(getDescriptor(optionalProperty.getValueRawType())));
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "getConverter",
+ "(L" + I_CLASS + ";)L" + I_CONVERTER + ";", false);
+ }
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertOptionalValue",
+ "(L" + I_STRING + ";L" + I_CONVERTER + ";)L" + I_OPTIONAL + ";", false);
+ mv.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType()));
+ mv.visitInsn(getReturnInstruction(property));
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ } else {
+ // There is no default, but we initialize an empty Optional inline in field
+ ctor.visitVarInsn(ALOAD, V_THIS);
+ ctor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false);
+ ctor.visitFieldInsn(PUTFIELD, className, memberName, "L" + I_OPTIONAL + ";");
+ }
+ } else if (property.isMap()) {
+ MapProperty mapProperty = property.asMap();
+ Property valueProperty = mapProperty.getValueProperty();
+ if (valueProperty.isLeaf()) {
+ if (mapProperty.hasDefaultValue() && mapProperty.getDefaultValue() != null) {
+ generateGetterWithDefaullt = true;
+ // Default Method
+ MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc,
+ null,
+ null);
+ mv.visitTypeInsn(NEW, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault");
+ mv.visitInsn(DUP);
+ mv.visitLdcInsn(mapProperty.getDefaultValue());
+ if (valueProperty.hasConvertWith()) {
+ String convertWith = getInternalName(valueProperty.asLeaf().getConvertWith());
+ mv.visitTypeInsn(NEW, convertWith);
+ mv.visitInsn(DUP);
+ mv.visitMethodInsn(INVOKESPECIAL, convertWith, "", "()V", false);
+ } else {
+ mv.visitLdcInsn(getType(valueProperty.asLeaf().getValueRawType()));
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "getConverter",
+ "(L" + I_CLASS + ";)L" + I_CONVERTER + ";", false);
+ }
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValue",
+ "(L" + I_STRING + ";L" + I_CONVERTER + ";)L" + I_OBJECT + ";", false);
+ mv.visitMethodInsn(INVOKESPECIAL, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault", "",
+ "(L" + I_OBJECT + ";)V", false);
+ mv.visitInsn(getReturnInstruction(property));
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ } else {
+ // There is no default, but we initialize an empty Map inline in field
+ ctor.visitVarInsn(ALOAD, V_THIS);
+ ctor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true);
+ ctor.visitFieldInsn(PUTFIELD, className, memberName, "L" + I_MAP + ";");
+ }
+ } else if (valueProperty.isCollection() && valueProperty.asCollection().getElement().isLeaf()) {
+ CollectionProperty collectionProperty = valueProperty.asCollection();
+ LeafProperty elementProperty = collectionProperty.getElement().asLeaf();
+ if (mapProperty.hasDefaultValue() && mapProperty.getDefaultValue() != null) {
+ generateGetterWithDefaullt = true;
+ // Default Method
+ MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc,
+ null,
+ null);
+ mv.visitTypeInsn(NEW, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault");
+ mv.visitInsn(DUP);
+ mv.visitLdcInsn(mapProperty.getDefaultValue());
+ if (elementProperty.hasConvertWith()) {
+ String convertWith = getInternalName(elementProperty.getConvertWith());
+ mv.visitTypeInsn(NEW, convertWith);
+ mv.visitInsn(DUP);
+ mv.visitMethodInsn(INVOKESPECIAL, convertWith, "", "()V", false);
+ } else {
+ mv.visitLdcInsn(getType(elementProperty.getValueRawType()));
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "getConverter",
+ "(L" + I_CLASS + ";)L" + I_CONVERTER + ";", false);
+ }
+ mv.visitLdcInsn(Type.getType(getDescriptor(collectionProperty.getCollectionRawType())));
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValues",
+ "(L" + I_STRING + ";L" + I_CONVERTER + ";L" + I_CLASS + ";)L" + I_COLLECTION + ";", false);
+ mv.visitMethodInsn(INVOKESPECIAL, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault", "",
+ "(L" + I_OBJECT + ";)V", false);
+ mv.visitInsn(getReturnInstruction(property));
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ } else {
+ // There is no default, but we initialize an empty Map inline in field
+ ctor.visitVarInsn(ALOAD, V_THIS);
+ ctor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true);
+ ctor.visitFieldInsn(PUTFIELD, className, memberName, "L" + I_MAP + ";");
+ }
+ } else if (valueProperty.isGroup())
+ if (mapProperty.hasDefaultValue()) {
+ generateGetterWithDefaullt = true;
+ // Default Method
+ MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc,
+ null,
+ null);
+ GroupProperty groupProperty = valueProperty.asGroup();
+ mv.visitTypeInsn(NEW, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault");
+ mv.visitInsn(DUP);
+ mv.visitLdcInsn(getType(groupProperty.getGroupType().getInterfaceType()));
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER, "forInterface",
+ "(L" + I_CLASS + ";)L" + I_CONFIG_INSTANCE_BUILDER + ";", true);
+ mv.visitMethodInsn(INVOKEINTERFACE, I_CONFIG_INSTANCE_BUILDER, "build", "()L" + I_OBJECT + ";",
+ true);
+ mv.visitTypeInsn(CHECKCAST, getInternalName(groupProperty.getGroupType().getInterfaceType()));
+ mv.visitMethodInsn(INVOKESPECIAL, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault", "",
+ "(L" + I_OBJECT + ";)V", false);
+ mv.visitInsn(getReturnInstruction(property));
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ } else {
+ // There is no default, but we initialize an empty Map inline in field
+ ctor.visitVarInsn(ALOAD, V_THIS);
+ ctor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true);
+ ctor.visitFieldInsn(PUTFIELD, className, memberName, "L" + I_MAP + ";");
+ }
+ } else if (property.isCollection() && property.asCollection().getElement().isLeaf()) {
+ CollectionProperty collectionProperty = property.asCollection();
+ LeafProperty elementProperty = collectionProperty.getElement().asLeaf();
+ if (elementProperty.hasDefaultValue() && elementProperty.getDefaultValue() != null) {
+ generateGetterWithDefaullt = true;
+ // Default Method
+ MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc, null,
+ null);
+ mv.visitLdcInsn(elementProperty.getDefaultValue());
+ if (elementProperty.hasConvertWith()) {
+ String convertWith = getInternalName(elementProperty.getConvertWith());
+ mv.visitTypeInsn(NEW, convertWith);
+ mv.visitInsn(DUP);
+ mv.visitMethodInsn(INVOKESPECIAL, convertWith, "", "()V", false);
+ } else {
+ mv.visitLdcInsn(Type.getType(getDescriptor(elementProperty.getValueRawType())));
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "getConverter",
+ "(L" + I_CLASS + ";)L" + I_CONVERTER + ";", false);
+ }
+ mv.visitLdcInsn(Type.getType(getDescriptor(collectionProperty.getCollectionRawType())));
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValues",
+ "(L" + I_STRING + ";L" + I_CONVERTER + ";L" + I_CLASS + ";)L" + I_COLLECTION + ";", false);
+ mv.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType()));
+ mv.visitInsn(getReturnInstruction(property));
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ }
+ } else if (property.isOptional() && property.asOptional().getNestedProperty().isCollection()
+ && property.asOptional().getNestedProperty().asCollection().getElement().isLeaf()) {
+ CollectionProperty collectionProperty = property.asOptional().getNestedProperty().asCollection();
+ LeafProperty elementProperty = collectionProperty.getElement().asLeaf();
+ if (elementProperty.hasDefaultValue() && elementProperty.getDefaultValue() != null) {
+ generateGetterWithDefaullt = true;
+ // Default Method
+ MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc, null,
+ null);
+ mv.visitLdcInsn(elementProperty.getDefaultValue());
+ if (elementProperty.hasConvertWith()) {
+ String convertWith = getInternalName(elementProperty.getConvertWith());
+ mv.visitTypeInsn(NEW, convertWith);
+ mv.visitInsn(DUP);
+ mv.visitMethodInsn(INVOKESPECIAL, convertWith, "", "()V", false);
+ } else {
+ mv.visitLdcInsn(Type.getType(getDescriptor(elementProperty.getValueRawType())));
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "getConverter",
+ "(L" + I_CLASS + ";)L" + I_CONVERTER + ";", false);
+ }
+ mv.visitLdcInsn(Type.getType(getDescriptor(collectionProperty.getCollectionRawType())));
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertOptionalValues",
+ "(L" + I_STRING + ";L" + I_CONVERTER + ";L" + I_CLASS + ";)L" + I_OPTIONAL + ";", false);
+ mv.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType()));
+ mv.visitInsn(getReturnInstruction(property));
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ } else {
+ // There is no default, but we initialize an empty Optional inline in field
+ ctor.visitVarInsn(ALOAD, V_THIS);
+ ctor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false);
+ ctor.visitFieldInsn(PUTFIELD, className, memberName, "L" + I_OPTIONAL + ";");
+ }
+ } else if (property.isGroup()) {
+ generateGetterWithDefaullt = true;
+ // Default Method
+ MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc, null,
+ null);
+ mv.visitLdcInsn(Type.getType(fieldDesc));
+ mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER, "forInterface",
+ "(L" + I_CLASS + ";)L" + I_CONFIG_INSTANCE_BUILDER + ";", true);
+ mv.visitMethodInsn(INVOKEINTERFACE, I_CONFIG_INSTANCE_BUILDER, "build", "()L" + I_OBJECT + ";", true);
+ mv.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType()));
+ mv.visitInsn(getReturnInstruction(property));
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ } else if (property.isOptional() && property.asOptional().getNestedProperty().isGroup()) {
+ // There is no default, but we initialize an empty Optional inline in field
+ ctor.visitVarInsn(ALOAD, V_THIS);
+ ctor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false);
+ ctor.visitFieldInsn(PUTFIELD, className, memberName, "L" + I_OPTIONAL + ";");
+ }
+
+ // Getter
+ MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC, memberName, "()" + fieldDesc, null, null);
+ if (generateGetterWithDefaullt) {
+ mv.visitVarInsn(ALOAD, V_THIS);
+ mv.visitFieldInsn(GETFIELD, className, memberName, fieldDesc);
+ Label _ifNull = new Label();
+ mv.visitJumpInsn(IFNULL, _ifNull);
+ mv.visitVarInsn(ALOAD, V_THIS);
+ mv.visitFieldInsn(GETFIELD, className, memberName, fieldDesc);
+ mv.visitInsn(ARETURN);
+ mv.visitLabel(_ifNull);
+ mv.visitFrame(F_SAME, 0, null, 0, null);
+ mv.visitMethodInsn(INVOKESTATIC, className, defaultMethodName, "()" + fieldDesc, false);
+ mv.visitInsn(ARETURN);
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ } else {
+ mv.visitVarInsn(ALOAD, V_THIS);
+ mv.visitFieldInsn(GETFIELD, className, memberName, fieldDesc);
+ mv.visitInsn(getReturnInstruction(property));
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ }
+ }
+
+ ctor.visitInsn(RETURN);
+ ctor.visitEnd();
+ ctor.visitMaxs(0, 0);
+
+ for (Property property : mapping.getProperties()) {
+ Method method = property.getMethod();
+ String memberName = method.getName();
+
+ // Field Declaration
+ String fieldDesc = getDescriptor(method.getReturnType());
+ visitor.visitField(ACC_PUBLIC, memberName, fieldDesc, null, null);
+
+ // Setter
+ MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC, memberName, "(" + fieldDesc + ")V", null, null);
+ mv.visitVarInsn(ALOAD, V_THIS);
+ switch (Type.getReturnType(method).getSort()) {
+ case Type.BOOLEAN,
+ Type.SHORT,
+ Type.CHAR,
+ Type.BYTE,
+ Type.INT ->
+ mv.visitVarInsn(ILOAD, 1);
+
+ case Type.LONG -> mv.visitVarInsn(LLOAD, 1);
+
+ case Type.FLOAT -> mv.visitVarInsn(FLOAD, 1);
+
+ case Type.DOUBLE -> mv.visitVarInsn(DLOAD, 1);
+
+ default -> mv.visitVarInsn(ALOAD, 1);
+ }
+ mv.visitFieldInsn(PUTFIELD, className, memberName, fieldDesc);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ }
+
+ return visitor.toByteArray();
}
/**
@@ -1293,146 +1710,4 @@ public String desc() {
return desc;
}
}
-
- 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(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/ConfigMappingInterface.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java
index b4f5f7c71..69958c1ac 100644
--- a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java
+++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java
@@ -48,6 +48,7 @@ protected ConfigMappingInterface computeValue(final Class> type) {
private final ConfigMappingInterface[] superTypes;
private final Property[] properties;
private final ToStringMethod toStringMethod;
+ private final List auxiliaryClasses;
ConfigMappingInterface(final Class> interfaceType, final ConfigMappingInterface[] superTypes,
final Property[] properties) {
@@ -69,6 +70,7 @@ protected ConfigMappingInterface computeValue(final Class> type) {
filteredProperties.sort(PropertyComparator.INSTANCE);
this.properties = collectFullHierarchyProperties(this, filteredProperties.toArray(Property[]::new));
this.toStringMethod = toStringMethod != null ? toStringMethod : ToStringMethod.NONE;
+ this.auxiliaryClasses = List.of(new ConfigMappingBuilder());
}
static String getImplementationClassName(Class> type) {
@@ -130,6 +132,18 @@ public Property[] getProperties() {
return properties;
}
+ /**
+ * Get a {@code List} of {@link io.smallrye.config.ConfigMappingMetadata} of auxiliary classes to load for this
+ * {@link io.smallrye.config.ConfigMappingInterface}, like dedicated builders for the mapping.
+ *
+ * @return a {@code List} of {@link io.smallrye.config.ConfigMappingMetadata} of auxiliary classes
+ *
+ * @see io.smallrye.config.ConfigMappingInterface.ConfigMappingBuilder
+ */
+ public List getAuxiliaryClasses() {
+ return auxiliaryClasses;
+ }
+
private static Property[] collectFullHierarchyProperties(final ConfigMappingInterface type, final Property[] properties) {
// We use a Map to override definitions from super members
// We want the properties to be sorted so that the iteration order is deterministic
@@ -196,6 +210,38 @@ public byte[] getClassBytes() {
}
}
+ class ConfigMappingBuilder implements ConfigMappingMetadata {
+ private final String builderClassName;
+
+ ConfigMappingBuilder() {
+ this.builderClassName = getBuilderClassName(ConfigMappingInterface.this.interfaceType);
+ }
+
+ @Override
+ public Class> getInterfaceType() {
+ return ConfigMappingInterface.this.interfaceType;
+ }
+
+ @Override
+ public String getClassName() {
+ return builderClassName;
+ }
+
+ @Override
+ public byte[] getClassBytes() {
+ return ConfigMappingGenerator.generateBuilder(ConfigMappingInterface.this, builderClassName.replace('.', '/'));
+ }
+
+ @Override
+ public List getAuxiliaryClasses() {
+ return Collections.emptyList();
+ }
+
+ static String getBuilderClassName(Class> type) {
+ return new StringBuilder(type.getName().length() + 11).append(type.getName()).append("$$CMBuilder").toString();
+ }
+ }
+
public static abstract class Property {
private final Method method;
private final String propertyName;
diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingLoader.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingLoader.java
index 8b86361d2..a11b4cf37 100644
--- a/implementation/src/main/java/io/smallrye/config/ConfigMappingLoader.java
+++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingLoader.java
@@ -8,6 +8,7 @@
import java.lang.reflect.Modifier;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -142,42 +143,50 @@ static Class> loadImplementation(final Class type) {
if (type.isAssignableFrom(implementationClass)) {
return implementationClass;
}
+ return loadMapping(type);
+ } catch (final ClassNotFoundException e) {
+ return loadMapping(type);
+ }
+ }
- ConfigMappingMetadata mappingMetadata = ConfigMappingInterface.getConfigurationInterface(type);
- if (mappingMetadata == null) {
- throw ConfigMessages.msg.classIsNotAMapping(type);
- }
- return loadClass(type, mappingMetadata);
- } catch (ClassNotFoundException e) {
- ConfigMappingMetadata mappingMetadata = ConfigMappingInterface.getConfigurationInterface(type);
- if (mappingMetadata == null) {
- throw ConfigMessages.msg.classIsNotAMapping(type);
- }
- return loadClass(type, mappingMetadata);
+ static Class> loadMapping(final Class type) {
+ ConfigMappingInterface mappingInterface = ConfigMappingInterface.getConfigurationInterface(type);
+ if (mappingInterface == null) {
+ throw ConfigMessages.msg.classIsNotAMapping(type);
}
+ return loadClass(type, mappingInterface);
}
- static Class> loadClass(final Class> parent, final ConfigMappingMetadata configMappingMetadata) {
+ static Class> loadClass(final Class> parent, final ConfigMappingMetadata configMapping) {
// acquire a lock on the class name to prevent race conditions in multithreaded use cases
- synchronized (getClassLoaderLock(configMappingMetadata.getClassName())) {
+ synchronized (getClassLoaderLock(configMapping.getClassName())) {
// Check if the interface implementation was already loaded. If not we will load it.
try {
- Class> klass = parent.getClassLoader().loadClass(configMappingMetadata.getClassName());
+ Class> klass = parent.getClassLoader().loadClass(configMapping.getClassName());
// Check if this is the right classloader class. If not we will load it.
if (parent.isAssignableFrom(klass)) {
return klass;
}
// ConfigProperties should not have issues with classloader and interfaces.
- if (configMappingMetadata instanceof ConfigMappingClass) {
+ if (configMapping instanceof ConfigMappingClass) {
return klass;
}
- return defineClass(parent, configMappingMetadata.getClassName(), configMappingMetadata.getClassBytes());
+
+ return loadClass(parent, configMapping, configMapping.getAuxiliaryClasses());
} catch (ClassNotFoundException e) {
- return defineClass(parent, configMappingMetadata.getClassName(), configMappingMetadata.getClassBytes());
+ return loadClass(parent, configMapping, configMapping.getAuxiliaryClasses());
}
}
}
+ private static Class> loadClass(final Class> parent, final ConfigMappingMetadata configMapping,
+ List auxiliaryClasses) {
+ for (ConfigMappingMetadata auxiliaryClass : auxiliaryClasses) {
+ defineClass(parent, auxiliaryClass.getClassName(), auxiliaryClass.getClassBytes());
+ }
+ return defineClass(parent, configMapping.getClassName(), configMapping.getClassBytes());
+ }
+
/**
* Do not remove this method or inline it. It is keep separate on purpose, so it is easier to substitute it with
* the GraalVM API for native image compilation.
@@ -293,5 +302,10 @@ public String getClassName() {
public byte[] getClassBytes() {
return ConfigMappingGenerator.generate(classType, interfaceName);
}
+
+ @Override
+ public List getAuxiliaryClasses() {
+ return Collections.emptyList();
+ }
}
}
diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingMetadata.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingMetadata.java
index a7384dd9d..3588a5f11 100644
--- a/implementation/src/main/java/io/smallrye/config/ConfigMappingMetadata.java
+++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingMetadata.java
@@ -1,9 +1,13 @@
package io.smallrye.config;
+import java.util.List;
+
public interface ConfigMappingMetadata {
Class> getInterfaceType();
String getClassName();
byte[] getClassBytes();
+
+ List getAuxiliaryClasses();
}
diff --git a/implementation/src/main/java/io/smallrye/config/_private/ConfigMessages.java b/implementation/src/main/java/io/smallrye/config/_private/ConfigMessages.java
index 52d54850f..7a2b1adcc 100644
--- a/implementation/src/main/java/io/smallrye/config/_private/ConfigMessages.java
+++ b/implementation/src/main/java/io/smallrye/config/_private/ConfigMessages.java
@@ -173,4 +173,36 @@ IllegalArgumentException converterException(@Cause Throwable converterException,
@Message(id = 51, value = "Could not generate ConfigMapping %s")
IllegalStateException couldNotGenerateMapping(@Cause Throwable throwable, String mapping);
+
+ @Message(id = 52, value = "Could not generate ConfigMapping")
+ IllegalStateException couldNotGenerateMapping(@Cause Throwable throwable);
+
+ @Message(id = 53, value = "Access to %2$s was denied in a modular environment. To avoid this error, edit "
+ + "`module-info.java` of %4$s to include `opens %3$s to %1$s`; or, add `--add-opens=%4$s/%3$s=%1$s` to "
+ + "the JVM command line.")
+ SecurityException accessDenied(String ourModuleName, Class> targetType, String targetPackage, String targetModuleName);
+
+ @Message(id = 54, value = "Access to %1$s was denied in a mixed-module environment. To avoid this error, "
+ + "add `--add-opens=%3$s/%2$s=ALL-UNNAMED` to the JVM command line.")
+ SecurityException accessDenied(Class> targetType, String targetPackage, String targetModuleName);
+
+ @Message(id = 55, value = "Missing a valid constructor on configuration implementation %s")
+ IllegalStateException noConstructor(Class> implClass);
+
+ @Message(id = 56, value = "The accessor for a configuration property is not valid")
+ IllegalArgumentException invalidGetter();
+
+ @Message(id = 57, value = "The property %s is required but it was not set in the ConfigInstanceBuilder")
+ NoSuchElementException propertyNotSet(String property);
+
+ default SecurityException accessDenied(Class> ourClass, Class> targetType) {
+ Module ourModule = ourClass.getModule();
+ Module targetModule = targetType.getModule();
+ assert targetModule.isNamed(); // otherwise we wouldn't be here
+ if (ourModule.isNamed()) {
+ return accessDenied(ourModule.getName(), targetType, targetType.getPackageName(), targetModule.getName());
+ } else {
+ return accessDenied(targetType, targetType.getPackageName(), targetModule.getName());
+ }
+ }
}
diff --git a/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderFullTest.java b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderFullTest.java
new file mode 100644
index 000000000..fd6a0832e
--- /dev/null
+++ b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderFullTest.java
@@ -0,0 +1,899 @@
+package io.smallrye.config;
+
+import static java.time.temporal.ChronoUnit.HOURS;
+import static java.time.temporal.ChronoUnit.MINUTES;
+import static java.time.temporal.ChronoUnit.SECONDS;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigInteger;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.time.format.DateTimeParseException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.microprofile.config.spi.Converter;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import io.smallrye.config.ConfigInstanceBuilderFullTest.HttpConfig.AuthRuntimeConfig.FormAuthConfig.CookieSameSite;
+import io.smallrye.config.ConfigInstanceBuilderFullTest.HttpConfig.AuthRuntimeConfig.InclusiveMode;
+import io.smallrye.config.ConfigInstanceBuilderFullTest.HttpConfig.CharsetConverter;
+import io.smallrye.config.ConfigInstanceBuilderFullTest.HttpConfig.DurationConverter;
+import io.smallrye.config.ConfigInstanceBuilderFullTest.HttpConfig.MemorySize;
+import io.smallrye.config.ConfigInstanceBuilderFullTest.HttpConfig.MemorySizeConverter;
+import io.smallrye.config.ConfigInstanceBuilderFullTest.HttpConfig.ProxyConfig.ForwardedPrecedence;
+
+class ConfigInstanceBuilderFullTest {
+ @BeforeAll
+ static void beforeAll() {
+ ConfigInstanceBuilder.registerConverter(Duration.class, new DurationConverter());
+ ConfigInstanceBuilder.registerConverter(MemorySize.class, new MemorySizeConverter());
+ ConfigInstanceBuilder.registerConverter(Charset.class, new CharsetConverter());
+ }
+
+ @Test
+ void emptyWithDefaults() {
+ HttpConfig httpConfig = ConfigInstanceBuilder.forInterface(HttpConfig.class)
+ .with(HttpConfig::host, "localhost")
+ .build();
+
+ assertEquals(8080, httpConfig.port());
+ assertEquals(8081, httpConfig.testPort());
+ assertEquals("localhost", httpConfig.host());
+ assertFalse(httpConfig.testHost().isPresent());
+ assertTrue(httpConfig.hostEnabled());
+ assertEquals(8443, httpConfig.sslPort());
+ assertEquals(8444, httpConfig.testSslPort());
+ assertFalse(httpConfig.testSslEnabled().isPresent());
+ assertFalse(httpConfig.insecureRequests().isPresent());
+ assertTrue(httpConfig.http2());
+ assertTrue(httpConfig.http2PushEnabled());
+ assertFalse(httpConfig.tlsConfigurationName().isPresent());
+ assertFalse(httpConfig.handle100ContinueAutomatically());
+ assertFalse(httpConfig.ioThreads().isPresent());
+ assertEquals(Duration.of(30, MINUTES), httpConfig.idleTimeout());
+ assertEquals(Duration.of(60, SECONDS), httpConfig.readTimeout());
+ assertFalse(httpConfig.encryptionKey().isPresent());
+ assertFalse(httpConfig.soReusePort());
+ assertFalse(httpConfig.tcpQuickAck());
+ assertFalse(httpConfig.tcpCork());
+ assertFalse(httpConfig.tcpFastOpen());
+ assertEquals(-1, httpConfig.acceptBacklog());
+ assertFalse(httpConfig.initialWindowSize().isPresent());
+ assertEquals("/var/run/io.quarkus.app.socket", httpConfig.domainSocket());
+ assertFalse(httpConfig.domainSocketEnabled());
+ assertFalse(httpConfig.recordRequestStartTime());
+ assertFalse(httpConfig.unhandledErrorContentTypeDefault().isPresent());
+
+ assertNotNull(httpConfig.auth());
+ assertNotNull(httpConfig.auth().permissions());
+ assertTrue(httpConfig.auth().permissions().isEmpty());
+ assertNotNull(httpConfig.auth().rolePolicy());
+ assertTrue(httpConfig.auth().rolePolicy().isEmpty());
+ assertNotNull(httpConfig.auth().rolesMapping());
+ assertTrue(httpConfig.auth().rolesMapping().isEmpty());
+ assertEquals("CN", httpConfig.auth().certificateRoleAttribute());
+ assertFalse(httpConfig.auth().certificateRoleProperties().isPresent());
+ assertFalse(httpConfig.auth().realm().isPresent());
+ assertNotNull(httpConfig.auth().form());
+ assertTrue(httpConfig.auth().form().loginPage().isPresent());
+ assertEquals("/login.html", httpConfig.auth().form().loginPage().get());
+ assertEquals("j_username", httpConfig.auth().form().usernameParameter());
+ assertEquals("j_password", httpConfig.auth().form().passwordParameter());
+ assertTrue(httpConfig.auth().form().errorPage().isPresent());
+ assertEquals("/error.html", httpConfig.auth().form().errorPage().get());
+ assertTrue(httpConfig.auth().form().landingPage().isPresent());
+ assertEquals("/index.html", httpConfig.auth().form().landingPage().get());
+ assertEquals("quarkus-redirect-location", httpConfig.auth().form().locationCookie());
+ assertEquals(Duration.of(30, MINUTES), httpConfig.auth().form().timeout());
+ assertEquals(Duration.of(1, MINUTES), httpConfig.auth().form().newCookieInterval());
+ assertEquals("quarkus-credential", httpConfig.auth().form().cookieName());
+ assertTrue(httpConfig.auth().form().cookiePath().isPresent());
+ assertEquals("/", httpConfig.auth().form().cookiePath().get());
+ assertFalse(httpConfig.auth().form().cookieDomain().isPresent());
+ assertFalse(httpConfig.auth().form().httpOnlyCookie());
+ assertEquals(CookieSameSite.STRICT, httpConfig.auth().form().cookieSameSite());
+ assertFalse(httpConfig.auth().form().cookieMaxAge().isPresent());
+ assertEquals("/j_security_check", httpConfig.auth().form().postLocation());
+ assertFalse(httpConfig.auth().inclusive());
+ assertEquals(InclusiveMode.STRICT, httpConfig.auth().inclusiveMode());
+
+ assertNotNull(httpConfig.cors());
+ assertFalse(httpConfig.cors().enabled());
+ assertFalse(httpConfig.cors().origins().isPresent());
+ assertFalse(httpConfig.cors().methods().isPresent());
+ assertFalse(httpConfig.cors().headers().isPresent());
+ assertFalse(httpConfig.cors().exposedHeaders().isPresent());
+ assertFalse(httpConfig.cors().accessControlMaxAge().isPresent());
+ assertFalse(httpConfig.cors().accessControlAllowCredentials().isPresent());
+
+ assertNotNull(httpConfig.ssl());
+ assertFalse(httpConfig.ssl().certificate().credentialsProvider().isPresent());
+ assertFalse(httpConfig.ssl().certificate().credentialsProviderName().isPresent());
+ assertFalse(httpConfig.ssl().certificate().files().isPresent());
+ assertFalse(httpConfig.ssl().certificate().keyFiles().isPresent());
+ assertFalse(httpConfig.ssl().certificate().keyStoreFile().isPresent());
+ assertFalse(httpConfig.ssl().certificate().keyStoreFileType().isPresent());
+ assertFalse(httpConfig.ssl().certificate().keyStoreProvider().isPresent());
+ assertFalse(httpConfig.ssl().certificate().keyStorePassword().isPresent());
+ assertFalse(httpConfig.ssl().certificate().keyStorePasswordKey().isPresent());
+ assertFalse(httpConfig.ssl().certificate().keyStoreAlias().isPresent());
+ assertFalse(httpConfig.ssl().certificate().keyStoreAliasPassword().isPresent());
+ assertFalse(httpConfig.ssl().certificate().keyStoreAliasPasswordKey().isPresent());
+ assertFalse(httpConfig.ssl().certificate().trustStoreFile().isPresent());
+ assertFalse(httpConfig.ssl().certificate().trustStoreFiles().isPresent());
+ assertFalse(httpConfig.ssl().certificate().trustStoreFileType().isPresent());
+ assertFalse(httpConfig.ssl().certificate().trustStoreProvider().isPresent());
+ assertFalse(httpConfig.ssl().certificate().trustStorePassword().isPresent());
+ assertFalse(httpConfig.ssl().certificate().trustStorePasswordKey().isPresent());
+ assertFalse(httpConfig.ssl().certificate().trustStoreCertAlias().isPresent());
+ assertFalse(httpConfig.ssl().certificate().reloadPeriod().isPresent());
+ assertFalse(httpConfig.ssl().cipherSuites().isPresent());
+ assertTrue(httpConfig.ssl().protocols().contains("TLSv1.2"));
+ assertTrue(httpConfig.ssl().protocols().contains("TLSv1.3"));
+ assertFalse(httpConfig.ssl().sni());
+
+ assertNotNull(httpConfig.staticResources());
+ assertEquals("index.html", httpConfig.staticResources().indexPage());
+ assertTrue(httpConfig.staticResources().includeHidden());
+ assertTrue(httpConfig.staticResources().enableRangeSupport());
+ assertTrue(httpConfig.staticResources().cachingEnabled());
+ assertEquals(Duration.of(30, SECONDS), httpConfig.staticResources().cacheEntryTimeout());
+ assertEquals(Duration.of(24, HOURS), httpConfig.staticResources().maxAge());
+ assertEquals(10000, httpConfig.staticResources().maxCacheSize());
+ assertEquals(StandardCharsets.UTF_8, httpConfig.staticResources().contentEncoding());
+
+ assertNotNull(httpConfig.limits());
+ assertEquals(new MemorySizeConverter().convert("20K"), httpConfig.limits().maxHeaderSize());
+ assertTrue(httpConfig.limits().maxBodySize().isPresent());
+ assertEquals(new MemorySizeConverter().convert("10240K"), httpConfig.limits().maxBodySize().get());
+ assertEquals(new MemorySizeConverter().convert("8192"), httpConfig.limits().maxChunkSize());
+ assertEquals(4096, httpConfig.limits().maxInitialLineLength());
+ assertEquals(new MemorySizeConverter().convert("2048"), httpConfig.limits().maxFormAttributeSize());
+ assertEquals(256, httpConfig.limits().maxFormFields());
+ assertEquals(new MemorySizeConverter().convert("1K"), httpConfig.limits().maxFormBufferedBytes());
+ assertEquals(1000, httpConfig.limits().maxParameters());
+ assertFalse(httpConfig.limits().maxConnections().isPresent());
+ assertFalse(httpConfig.limits().headerTableSize().isPresent());
+ assertFalse(httpConfig.limits().maxConcurrentStreams().isPresent());
+ assertFalse(httpConfig.limits().maxFrameSize().isPresent());
+ assertFalse(httpConfig.limits().maxHeaderListSize().isPresent());
+ assertFalse(httpConfig.limits().rstFloodMaxRstFramePerWindow().isPresent());
+ assertFalse(httpConfig.limits().rstFloodWindowDuration().isPresent());
+
+ assertNotNull(httpConfig.body());
+ assertTrue(httpConfig.body().handleFileUploads());
+ // TODO - how to handle expressions?
+ assertEquals("${java.io.tmpdir}/uploads", httpConfig.body().uploadsDirectory());
+ assertTrue(httpConfig.body().mergeFormAttributes());
+ assertTrue(httpConfig.body().deleteUploadedFilesOnEnd());
+ assertFalse(httpConfig.body().preallocateBodyBuffer());
+ assertNotNull(httpConfig.body().multipart());
+ assertFalse(httpConfig.body().multipart().fileContentTypes().isPresent());
+
+ assertNotNull(httpConfig.accessLog());
+ assertFalse(httpConfig.accessLog().enabled());
+ assertFalse(httpConfig.accessLog().excludePattern().isPresent());
+ assertEquals("common", httpConfig.accessLog().pattern());
+ assertFalse(httpConfig.accessLog().logToFile());
+ assertEquals("quarkus", httpConfig.accessLog().baseFileName());
+ assertFalse(httpConfig.accessLog().logDirectory().isPresent());
+ assertEquals(".log", httpConfig.accessLog().logSuffix());
+ assertEquals("io.quarkus.http.access-log", httpConfig.accessLog().category());
+ assertTrue(httpConfig.accessLog().rotate());
+ assertFalse(httpConfig.accessLog().consolidateReroutedRequests());
+
+ assertNotNull(httpConfig.trafficShaping());
+ assertFalse(httpConfig.trafficShaping().enabled());
+ assertFalse(httpConfig.trafficShaping().inboundGlobalBandwidth().isPresent());
+ assertFalse(httpConfig.trafficShaping().outboundGlobalBandwidth().isPresent());
+ assertFalse(httpConfig.trafficShaping().maxDelay().isPresent());
+ assertFalse(httpConfig.trafficShaping().checkInterval().isPresent());
+ assertFalse(httpConfig.trafficShaping().peakOutboundGlobalBandwidth().isPresent());
+
+ assertNotNull(httpConfig.sameSiteCookie());
+ assertTrue(httpConfig.sameSiteCookie().isEmpty());
+ assertNotNull(httpConfig.header());
+ assertTrue(httpConfig.header().isEmpty());
+ assertNotNull(httpConfig.filter());
+ assertTrue(httpConfig.filter().isEmpty());
+
+ assertNotNull(httpConfig.proxy());
+ assertFalse(httpConfig.proxy().useProxyProtocol());
+ assertFalse(httpConfig.proxy().proxyAddressForwarding());
+ assertFalse(httpConfig.proxy().allowForwarded());
+ assertFalse(httpConfig.proxy().allowXForwarded().isPresent());
+ assertTrue(httpConfig.proxy().strictForwardedControl());
+ assertEquals(ForwardedPrecedence.FORWARDED, httpConfig.proxy().forwardedPrecedence());
+ assertFalse(httpConfig.proxy().enableForwardedHost());
+ assertEquals("X-Forwarded-Host", httpConfig.proxy().forwardedHostHeader());
+ assertFalse(httpConfig.proxy().enableForwardedPrefix());
+ assertEquals("X-Forwarded-Prefix", httpConfig.proxy().forwardedPrefixHeader());
+ assertFalse(httpConfig.proxy().enableTrustedProxyHeader());
+
+ assertNotNull(httpConfig.websocketServer());
+ assertFalse(httpConfig.websocketServer().maxFrameSize().isPresent());
+ assertFalse(httpConfig.websocketServer().maxMessageSize().isPresent());
+ }
+
+ @ConfigMapping
+ interface HttpConfig {
+ AuthRuntimeConfig auth();
+
+ @WithDefault("8080")
+ int port();
+
+ @WithDefault("8081")
+ int testPort();
+
+ String host();
+
+ Optional testHost();
+
+ @WithDefault("true")
+ boolean hostEnabled();
+
+ @WithDefault("8443")
+ int sslPort();
+
+ @WithDefault("8444")
+ int testSslPort();
+
+ Optional testSslEnabled();
+
+ Optional insecureRequests();
+
+ @WithDefault("true")
+ boolean http2();
+
+ @WithDefault("true")
+ boolean http2PushEnabled();
+
+ CORSConfig cors();
+
+ ServerSslConfig ssl();
+
+ Optional tlsConfigurationName();
+
+ StaticResourcesConfig staticResources();
+
+ @WithName("handle-100-continue-automatically")
+ @WithDefault("false")
+ boolean handle100ContinueAutomatically();
+
+ OptionalInt ioThreads();
+
+ ServerLimitsConfig limits();
+
+ @WithDefault("30M")
+ Duration idleTimeout();
+
+ @WithDefault("60s")
+ Duration readTimeout();
+
+ BodyConfig body();
+
+ @WithName("auth.session.encryption-key")
+ Optional encryptionKey();
+
+ @WithDefault("false")
+ boolean soReusePort();
+
+ @WithDefault("false")
+ boolean tcpQuickAck();
+
+ @WithDefault("false")
+ boolean tcpCork();
+
+ @WithDefault("false")
+ boolean tcpFastOpen();
+
+ @WithDefault("-1")
+ int acceptBacklog();
+
+ OptionalInt initialWindowSize();
+
+ @WithDefault("/var/run/io.quarkus.app.socket")
+ String domainSocket();
+
+ @WithDefault("false")
+ boolean domainSocketEnabled();
+
+ @WithDefault("false")
+ boolean recordRequestStartTime();
+
+ AccessLogConfig accessLog();
+
+ TrafficShapingConfig trafficShaping();
+
+ Map sameSiteCookie();
+
+ Optional unhandledErrorContentTypeDefault();
+
+ Map header();
+
+ Map filter();
+
+ ProxyConfig proxy();
+
+ WebsocketServerConfig websocketServer();
+
+ interface AuthRuntimeConfig {
+ @WithName("permission")
+ Map permissions();
+
+ @WithName("policy")
+ Map rolePolicy();
+
+ Map> rolesMapping();
+
+ @WithDefault("CN")
+ String certificateRoleAttribute();
+
+ Optional certificateRoleProperties();
+
+ Optional realm();
+
+ FormAuthConfig form();
+
+ @WithDefault("false")
+ boolean inclusive();
+
+ @WithDefault("strict")
+ InclusiveMode inclusiveMode();
+
+ interface PolicyMappingConfig {
+ Optional enabled();
+
+ String policy();
+
+ Optional> methods();
+
+ Optional> paths();
+
+ Optional authMechanism();
+
+ @WithDefault("false")
+ boolean shared();
+
+ @WithDefault("ALL")
+ AppliesTo appliesTo();
+
+ enum AppliesTo {
+ ALL,
+ JAXRS
+ }
+ }
+
+ interface PolicyConfig {
+ @WithDefault("**")
+ List rolesAllowed();
+
+ Map> roles();
+
+ Map> permissions();
+
+ @WithDefault("io.quarkus.security.StringPermission")
+ String permissionClass();
+ }
+
+ interface FormAuthConfig {
+ enum CookieSameSite {
+ STRICT,
+ LAX,
+ NONE
+ }
+
+ @WithDefault("/login.html")
+ Optional loginPage();
+
+ @WithDefault("j_username")
+ String usernameParameter();
+
+ @WithDefault("j_password")
+ String passwordParameter();
+
+ @WithDefault("/error.html")
+ Optional errorPage();
+
+ @WithDefault("/index.html")
+ Optional landingPage();
+
+ @WithDefault("quarkus-redirect-location")
+ String locationCookie();
+
+ @WithDefault("PT30M")
+ Duration timeout();
+
+ @WithDefault("PT1M")
+ Duration newCookieInterval();
+
+ @WithDefault("quarkus-credential")
+ String cookieName();
+
+ @WithDefault("/")
+ Optional cookiePath();
+
+ Optional cookieDomain();
+
+ @WithDefault("false")
+ boolean httpOnlyCookie();
+
+ @WithDefault("strict")
+ CookieSameSite cookieSameSite();
+
+ Optional cookieMaxAge();
+
+ @WithDefault("/j_security_check")
+ String postLocation();
+ }
+
+ enum InclusiveMode {
+ LAX,
+ STRICT
+ }
+ }
+
+ interface CORSConfig {
+ @WithDefault("false")
+ boolean enabled();
+
+ Optional> origins();
+
+ Optional> methods();
+
+ Optional> headers();
+
+ Optional> exposedHeaders();
+
+ Optional accessControlMaxAge();
+
+ Optional accessControlAllowCredentials();
+ }
+
+ interface ServerSslConfig {
+ CertificateConfig certificate();
+
+ Optional> cipherSuites();
+
+ @WithDefault("TLSv1.3,TLSv1.2")
+ Set protocols();
+
+ @WithDefault("false")
+ boolean sni();
+
+ interface CertificateConfig {
+ Optional credentialsProvider();
+
+ Optional credentialsProviderName();
+
+ Optional> files();
+
+ Optional> keyFiles();
+
+ Optional keyStoreFile();
+
+ Optional keyStoreFileType();
+
+ Optional keyStoreProvider();
+
+ Optional keyStorePassword();
+
+ Optional keyStorePasswordKey();
+
+ Optional keyStoreAlias();
+
+ Optional keyStoreAliasPassword();
+
+ Optional keyStoreAliasPasswordKey();
+
+ Optional trustStoreFile();
+
+ Optional> trustStoreFiles();
+
+ Optional trustStoreFileType();
+
+ Optional trustStoreProvider();
+
+ Optional trustStorePassword();
+
+ Optional trustStorePasswordKey();
+
+ Optional trustStoreCertAlias();
+
+ Optional reloadPeriod();
+ }
+ }
+
+ interface StaticResourcesConfig {
+ @WithDefault("index.html")
+ String indexPage();
+
+ @WithDefault("true")
+ boolean includeHidden();
+
+ @WithDefault("true")
+ boolean enableRangeSupport();
+
+ @WithDefault("true")
+ boolean cachingEnabled();
+
+ @WithDefault("30S")
+ Duration cacheEntryTimeout();
+
+ @WithDefault("24H")
+ Duration maxAge();
+
+ @WithDefault("10000")
+ int maxCacheSize();
+
+ @WithDefault("UTF-8")
+ Charset contentEncoding();
+ }
+
+ interface ServerLimitsConfig {
+ @WithDefault("20K")
+ MemorySize maxHeaderSize();
+
+ @WithDefault("10240K")
+ Optional maxBodySize();
+
+ @WithDefault("8192")
+ MemorySize maxChunkSize();
+
+ @WithDefault("4096")
+ int maxInitialLineLength();
+
+ @WithDefault("2048")
+ MemorySize maxFormAttributeSize();
+
+ @WithDefault("256")
+ int maxFormFields();
+
+ @WithDefault("1K")
+ MemorySize maxFormBufferedBytes();
+
+ @WithDefault("1000")
+ int maxParameters();
+
+ OptionalInt maxConnections();
+
+ OptionalLong headerTableSize();
+
+ OptionalLong maxConcurrentStreams();
+
+ OptionalInt maxFrameSize();
+
+ OptionalLong maxHeaderListSize();
+
+ OptionalInt rstFloodMaxRstFramePerWindow();
+
+ Optional rstFloodWindowDuration();
+ }
+
+ interface BodyConfig {
+ @WithDefault("true")
+ boolean handleFileUploads();
+
+ @WithDefault("${java.io.tmpdir}/uploads")
+ String uploadsDirectory();
+
+ @WithDefault("true")
+ boolean mergeFormAttributes();
+
+ @WithDefault("true")
+ boolean deleteUploadedFilesOnEnd();
+
+ @WithDefault("false")
+ boolean preallocateBodyBuffer();
+
+ MultiPartConfig multipart();
+
+ interface MultiPartConfig {
+ Optional> fileContentTypes();
+ }
+ }
+
+ interface AccessLogConfig {
+ @WithDefault("false")
+ boolean enabled();
+
+ Optional excludePattern();
+
+ @WithDefault("common")
+ String pattern();
+
+ @WithDefault("false")
+ boolean logToFile();
+
+ @WithDefault("quarkus")
+ String baseFileName();
+
+ Optional logDirectory();
+
+ @WithDefault(".log")
+ String logSuffix();
+
+ @WithDefault("io.quarkus.http.access-log")
+ String category();
+
+ @WithDefault("true")
+ boolean rotate();
+
+ @WithDefault("false")
+ boolean consolidateReroutedRequests();
+ }
+
+ interface TrafficShapingConfig {
+ @WithDefault("false")
+ boolean enabled();
+
+ Optional inboundGlobalBandwidth();
+
+ Optional outboundGlobalBandwidth();
+
+ Optional maxDelay();
+
+ Optional checkInterval();
+
+ Optional peakOutboundGlobalBandwidth();
+ }
+
+ interface SameSiteCookieConfig {
+ @WithDefault("false")
+ boolean caseSensitive();
+
+ CookieSameSite value();
+
+ @WithDefault("true")
+ boolean enableClientChecker();
+
+ @WithDefault("true")
+ boolean addSecureForNone();
+ }
+
+ interface HeaderConfig {
+ @WithDefault("/*")
+ String path();
+
+ String value();
+
+ Optional> methods();
+ }
+
+ interface FilterConfig {
+ String matches();
+
+ Map header();
+
+ Optional> methods();
+
+ OptionalInt order();
+ }
+
+ interface ProxyConfig {
+ @WithDefault("false")
+ boolean useProxyProtocol();
+
+ @WithDefault("false")
+ boolean proxyAddressForwarding();
+
+ @WithDefault("false")
+ boolean allowForwarded();
+
+ Optional allowXForwarded();
+
+ @WithDefault("true")
+ boolean strictForwardedControl();
+
+ enum ForwardedPrecedence {
+ FORWARDED,
+ X_FORWARDED
+ }
+
+ @WithDefault("forwarded")
+ ForwardedPrecedence forwardedPrecedence();
+
+ @WithDefault("false")
+ boolean enableForwardedHost();
+
+ @WithDefault("X-Forwarded-Host")
+ String forwardedHostHeader();
+
+ @WithDefault("false")
+ boolean enableForwardedPrefix();
+
+ @WithDefault("X-Forwarded-Prefix")
+ String forwardedPrefixHeader();
+
+ @WithDefault("false")
+ boolean enableTrustedProxyHeader();
+ }
+
+ interface WebsocketServerConfig {
+ Optional maxFrameSize();
+
+ Optional maxMessageSize();
+ }
+
+ enum InsecureRequests {
+ ENABLED,
+ REDIRECT,
+ DISABLED;
+ }
+
+ enum PayloadHint {
+ JSON,
+ HTML,
+ TEXT
+ }
+
+ enum CookieSameSite {
+ NONE("None"),
+ STRICT("Strict"),
+ LAX("Lax");
+
+ private final String label;
+
+ private CookieSameSite(String label) {
+ this.label = label;
+ }
+
+ public String toString() {
+ return this.label;
+ }
+ }
+
+ final class MemorySize {
+ private final BigInteger value;
+
+ public MemorySize(BigInteger value) {
+ this.value = value;
+ }
+
+ public long asLongValue() {
+ return value.longValueExact();
+ }
+
+ public BigInteger asBigInteger() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object)
+ return true;
+ if (object == null || getClass() != object.getClass())
+ return false;
+ MemorySize that = (MemorySize) object;
+ return Objects.equals(value, that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(value);
+ }
+ }
+
+ class DurationConverter implements Converter, Serializable {
+ @Serial
+ private static final long serialVersionUID = 7499347081928776532L;
+ private static final String PERIOD = "P";
+ private static final String PERIOD_OF_TIME = "PT";
+ public static final Pattern DIGITS = Pattern.compile("^[-+]?\\d+$");
+ private static final Pattern DIGITS_AND_UNIT = Pattern.compile("^(?:[-+]?\\d+(?:\\.\\d+)?(?i)[hms])+$");
+ private static final Pattern DAYS = Pattern.compile("^[-+]?\\d+(?i)d$");
+ private static final Pattern MILLIS = Pattern.compile("^[-+]?\\d+(?i)ms$");
+
+ public DurationConverter() {
+ }
+
+ @Override
+ public Duration convert(String value) {
+ return parseDuration(value);
+ }
+
+ public static Duration parseDuration(String value) {
+ value = value.trim();
+ if (value.isEmpty()) {
+ return null;
+ }
+ if (DIGITS.asPredicate().test(value)) {
+ return Duration.ofSeconds(Long.parseLong(value));
+ } else if (MILLIS.asPredicate().test(value)) {
+ return Duration.ofMillis(Long.parseLong(value.substring(0, value.length() - 2)));
+ }
+
+ try {
+ if (DIGITS_AND_UNIT.asPredicate().test(value)) {
+ return Duration.parse(PERIOD_OF_TIME + value);
+ } else if (DAYS.asPredicate().test(value)) {
+ return Duration.parse(PERIOD + value);
+ }
+
+ return Duration.parse(value);
+ } catch (DateTimeParseException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+ }
+
+ class MemorySizeConverter implements Converter, Serializable {
+ @Serial
+ private static final long serialVersionUID = -1988485929047973068L;
+ private static final Pattern MEMORY_SIZE_PATTERN = Pattern.compile("^(\\d+)([BbKkMmGgTtPpEeZzYy]?)$");
+ static final BigInteger KILO_BYTES = BigInteger.valueOf(1024);
+ private static final Map MEMORY_SIZE_MULTIPLIERS;
+
+ static {
+ MEMORY_SIZE_MULTIPLIERS = new HashMap<>();
+ MEMORY_SIZE_MULTIPLIERS.put("K", KILO_BYTES);
+ MEMORY_SIZE_MULTIPLIERS.put("M", KILO_BYTES.pow(2));
+ MEMORY_SIZE_MULTIPLIERS.put("G", KILO_BYTES.pow(3));
+ MEMORY_SIZE_MULTIPLIERS.put("T", KILO_BYTES.pow(4));
+ MEMORY_SIZE_MULTIPLIERS.put("P", KILO_BYTES.pow(5));
+ MEMORY_SIZE_MULTIPLIERS.put("E", KILO_BYTES.pow(6));
+ MEMORY_SIZE_MULTIPLIERS.put("Z", KILO_BYTES.pow(7));
+ MEMORY_SIZE_MULTIPLIERS.put("Y", KILO_BYTES.pow(8));
+ }
+
+ public MemorySize convert(String value) {
+ value = value.trim();
+ if (value.isEmpty()) {
+ return null;
+ }
+ Matcher matcher = MEMORY_SIZE_PATTERN.matcher(value);
+ if (matcher.find()) {
+ BigInteger number = new BigInteger(matcher.group(1));
+ String scale = matcher.group(2).toUpperCase();
+ BigInteger multiplier = MEMORY_SIZE_MULTIPLIERS.get(scale);
+ return multiplier == null ? new MemorySize(number) : new MemorySize(number.multiply(multiplier));
+ }
+
+ throw new IllegalArgumentException(
+ String.format("value %s not in correct format (regular expression): [0-9]+[BbKkMmGgTtPpEeZzYy]?",
+ value));
+ }
+ }
+
+ class CharsetConverter implements Converter, Serializable {
+ @Serial
+ private static final long serialVersionUID = 2320905063828247874L;
+
+ @Override
+ public Charset convert(String value) {
+ if (value == null) {
+ return null;
+ }
+
+ String trimmedCharset = value.trim();
+
+ if (trimmedCharset.isEmpty()) {
+ return null;
+ }
+
+ try {
+ return Charset.forName(trimmedCharset);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Unable to create Charset from: '" + trimmedCharset + "'", e);
+ }
+ }
+ }
+ }
+}
diff --git a/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java
new file mode 100644
index 000000000..21d4605cc
--- /dev/null
+++ b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java
@@ -0,0 +1,428 @@
+package io.smallrye.config;
+
+import static io.smallrye.config.ConfigInstanceBuilder.forInterface;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import java.util.Set;
+
+import org.eclipse.microprofile.config.spi.Converter;
+import org.junit.jupiter.api.Test;
+
+import io.smallrye.config.ConfigInstanceBuilderTest.ConverterNotFound.NotFound;
+import io.smallrye.config.ConfigInstanceBuilderTest.Converters.Numbers;
+
+class ConfigInstanceBuilderTest {
+ @Test
+ void builder() {
+ Server server = forInterface(Server.class)
+ .with(Server::host, "localhost")
+ .with(Server::port, 8080)
+ .build();
+
+ assertEquals("localhost", server.host());
+ assertEquals(8080, server.port());
+ }
+
+ @ConfigMapping
+ interface Server {
+ String host();
+
+ int port();
+ }
+
+ @Test
+ void primitives() {
+ Primitives primitives = forInterface(Primitives.class)
+ .with(Primitives::booleanValue, true)
+ .with(Primitives::byteValue, Byte.valueOf((byte) 1))
+ .with(Primitives::shortValue, Short.valueOf((short) 1))
+ .with(Primitives::intValue, Integer.valueOf(1))
+ .with(Primitives::longValue, Long.valueOf(1))
+ .with(Primitives::floatValue, Float.valueOf((float) 1.0))
+ .with(Primitives::doubleValue, 1.0d)
+ .with(Primitives::charValue, Character.valueOf((char) 1))
+ .with(Primitives::stringValue, "value")
+ .build();
+
+ assertTrue(primitives.booleanValue());
+ assertEquals(Byte.valueOf((byte) 1), primitives.byteValue());
+ assertEquals(Short.valueOf((short) 1), primitives.shortValue());
+ assertEquals(Integer.valueOf(1), primitives.intValue());
+ assertEquals(Long.valueOf(1), primitives.longValue());
+ assertEquals(Float.valueOf((float) 1.0), primitives.floatValue());
+ assertEquals(Double.valueOf(1.0), primitives.doubleValue());
+ assertEquals(Character.valueOf((char) 1), primitives.charValue());
+ assertEquals("value", primitives.stringValue());
+ }
+
+ @ConfigMapping
+ interface Primitives {
+ boolean booleanValue();
+
+ byte byteValue();
+
+ short shortValue();
+
+ int intValue();
+
+ long longValue();
+
+ float floatValue();
+
+ double doubleValue();
+
+ char charValue();
+
+ String stringValue();
+ }
+
+ @Test
+ void nested() {
+ Nested nested = forInterface(Nested.class)
+ .with(Nested::value, "value")
+ .with(Nested::group, forInterface(Nested.Group.class).with(Nested.Group::value, "group").build())
+ .build();
+
+ assertEquals("value", nested.value());
+ assertEquals("group", nested.group().value());
+ }
+
+ @ConfigMapping
+ interface Nested {
+ String value();
+
+ Group group();
+
+ interface Group {
+ String value();
+ }
+ }
+
+ @Test
+ void optionals() {
+ Optionals optionals = forInterface(Optionals.class)
+ .withOptional(Optionals::optional, "value")
+ .withOptional(Optionals::optionalInt, 1)
+ .withOptional(Optionals::optionalLong, 1)
+ .withOptional(Optionals::optionalDouble, 1.1d)
+ .withOptional(Optionals::optionalBoolean, true)
+ .withOptional(Optionals::group, ConfigInstanceBuilder.forInterface(Optionals.Group.class)
+ .with(Optionals.Group::value, "value")
+ .build())
+ .build();
+
+ assertTrue(optionals.optional().isPresent());
+ assertEquals("value", optionals.optional().get());
+ assertTrue(optionals.optionalInt().isPresent());
+ assertEquals(1, optionals.optionalInt().getAsInt());
+ assertTrue(optionals.optionalLong().isPresent());
+ assertEquals(1L, optionals.optionalLong().getAsLong());
+ assertTrue(optionals.optionalDouble().isPresent());
+ assertEquals(1.1d, optionals.optionalDouble().getAsDouble());
+ assertTrue(optionals.optionalBoolean().isPresent());
+ assertTrue(optionals.optionalBoolean().get());
+ assertTrue(optionals.group().isPresent());
+ assertEquals("value", optionals.group().get().value());
+
+ Optionals empty = forInterface(Optionals.class).build();
+ assertTrue(empty.optional().isEmpty());
+ assertTrue(empty.optionalInt().isEmpty());
+ assertTrue(empty.optionalLong().isEmpty());
+ assertTrue(empty.optionalDouble().isEmpty());
+ assertTrue(empty.optionalBoolean().isEmpty());
+ assertTrue(empty.group().isEmpty());
+ }
+
+ @ConfigMapping
+ interface Optionals {
+ Optional optional();
+
+ OptionalInt optionalInt();
+
+ OptionalLong optionalLong();
+
+ OptionalDouble optionalDouble();
+
+ Optional optionalBoolean();
+
+ Optional group();
+
+ interface Group {
+ String value();
+ }
+ }
+
+ @Test
+ void defaults() {
+ Defaults defaults = forInterface(Defaults.class).build();
+ assertEquals("value", defaults.value());
+ assertEquals(9, defaults.defaultInt());
+ assertEquals("nested", defaults.nested().value());
+ }
+
+ @ConfigMapping
+ interface Defaults {
+ @WithDefault("value")
+ String value();
+
+ @WithDefault("9")
+ int defaultInt();
+
+ Nested nested();
+
+ interface Nested {
+ @WithDefault("nested")
+ String value();
+ }
+ }
+
+ @Test
+ void optionalDefaults() {
+ OptionalDefaults optionalDefaults = forInterface(OptionalDefaults.class).build();
+
+ assertTrue(optionalDefaults.optional().isPresent());
+ assertEquals("value", optionalDefaults.optional().get());
+ assertTrue(optionalDefaults.optionalInt().isPresent());
+ assertEquals(10, optionalDefaults.optionalInt().getAsInt());
+ assertTrue(optionalDefaults.optionalLong().isPresent());
+ assertEquals(10L, optionalDefaults.optionalLong().getAsLong());
+ assertTrue(optionalDefaults.optionalDouble().isPresent());
+ assertEquals(10.10d, optionalDefaults.optionalDouble().getAsDouble());
+ assertTrue(optionalDefaults.optionalList().isPresent());
+ assertIterableEquals(List.of("one", "two", "three"), optionalDefaults.optionalList().get());
+ }
+
+ interface OptionalDefaults {
+ @WithDefault("value")
+ Optional optional();
+
+ @WithDefault("10")
+ OptionalInt optionalInt();
+
+ @WithDefault("10")
+ OptionalLong optionalLong();
+
+ @WithDefault("10.10")
+ OptionalDouble optionalDouble();
+
+ @WithDefault("one,two,three")
+ Optional> optionalList();
+ }
+
+ @Test
+ void collections() {
+ Collections collections = forInterface(Collections.class)
+ .with(Collections::empty, List. of())
+ .with(Collections::list, List.of("one", "two", "three"))
+ .build();
+
+ assertIterableEquals(List.of("one", "two", "three"), collections.list());
+ assertNotNull(collections.empty());
+ assertTrue(collections.empty().isEmpty());
+ assertIterableEquals(List.of("one", "two", "three"), collections.defaults());
+ assertTrue(collections.setDefaults().contains("one"));
+
+ assertThrows(NoSuchElementException.class, () -> forInterface(Collections.class).build());
+ }
+
+ @ConfigMapping
+ interface Collections {
+ List list();
+
+ List empty();
+
+ @WithDefault("one,two,three")
+ List defaults();
+
+ @WithDefault("one")
+ Set setDefaults();
+ }
+
+ @Test
+ void maps() {
+ Maps maps = forInterface(Maps.class)
+ .with(Maps::map, Map.of("one", "one", "two", "two"))
+ .with(Maps::nested, Map.of("one", Map. of()))
+ .build();
+
+ assertEquals("one", maps.map().get("one"));
+ assertEquals("two", maps.map().get("two"));
+ assertEquals("value", maps.defaults().get("one"));
+ assertEquals("value", maps.defaults().get("two"));
+ assertEquals("value", maps.defaults().get("three"));
+ assertEquals(10, maps.mapIntegers().get("default"));
+ assertEquals("value", maps.group().get("any").value());
+ assertIterableEquals(List.of("one", "two", "three"), maps.mapLists().get("any"));
+ assertIterableEquals(List.of(1, 2, 3), maps.mapListsIntegers().get("any"));
+
+ assertThrows(NoSuchElementException.class, () -> forInterface(Maps.class)
+ .with(Maps::map, Map.of("one", "one", "two", "two"))
+ .build());
+ }
+
+ @ConfigMapping
+ interface Maps {
+ Map map();
+
+ Map empty();
+
+ @WithDefault("value")
+ Map defaults();
+
+ @WithDefault("10")
+ Map mapIntegers();
+
+ @WithDefaults
+ Map group();
+
+ // TODO - Add defaults for middle maps?
+ @WithDefault("any")
+ Map> nested();
+
+ @WithDefault("one,two,three")
+ Map> mapLists();
+
+ @WithDefault("1,2,3")
+ Map> mapListsIntegers();
+
+ interface Group {
+ @WithDefault("value")
+ String value();
+ }
+ }
+
+ @Test
+ void converters() {
+ Converters converters = forInterface(Converters.class).build();
+
+ assertEquals("converted", converters.value());
+ assertEquals(999, converters.intValue());
+ assertEquals(Numbers.ONE, converters.numbers());
+ assertEquals(Numbers.THREE, converters.numbersOverride());
+ assertTrue(converters.optional().isPresent());
+ assertEquals("converted", converters.optional().get());
+ assertTrue(converters.optionalInt().isPresent());
+ assertEquals(999, converters.optionalInt().get());
+ assertTrue(converters.optionalList().isPresent());
+ assertIterableEquals(List.of("converted", "converted", "converted"), converters.optionalList().get());
+ assertIterableEquals(List.of("converted", "converted", "converted"), converters.list());
+ assertIterableEquals(List.of(999, 999, 999), converters.listInt());
+ assertEquals("converted", converters.map().get("default"));
+ assertEquals("converted", converters.map().get("any"));
+ assertEquals(999, converters.mapInt().get("default"));
+ assertEquals(999, converters.mapInt().get("any"));
+ assertIterableEquals(List.of("converted", "converted", "converted"), converters.mapList().get("default"));
+ assertIterableEquals(List.of("converted", "converted", "converted"), converters.mapList().get("any"));
+ assertIterableEquals(List.of(999, 999, 999), converters.mapListInt().get("default"));
+ assertIterableEquals(List.of(999, 999, 999), converters.mapListInt().get("any"));
+ }
+
+ @ConfigMapping
+ interface Converters {
+ @WithDefault("to-convert")
+ @WithConverter(StringValueConverter.class)
+ String value();
+
+ @WithDefault("to-convert")
+ @WithConverter(IntegerValueConverter.class)
+ int intValue();
+
+ @WithDefault("one")
+ Numbers numbers();
+
+ @WithDefault("to-convert")
+ @WithConverter(NumbersConverter.class)
+ Numbers numbersOverride();
+
+ @WithDefault("to-convert")
+ Optional<@WithConverter(StringValueConverter.class) String> optional();
+
+ @WithDefault("to-convert")
+ Optional<@WithConverter(IntegerValueConverter.class) Integer> optionalInt();
+
+ @WithDefault("one,two,three")
+ Optional<@WithConverter(StringValueConverter.class) List> optionalList();
+
+ @WithDefault("one,two,three")
+ List<@WithConverter(StringValueConverter.class) String> list();
+
+ @WithDefault("1,2,3")
+ List<@WithConverter(IntegerValueConverter.class) Integer> listInt();
+
+ @WithDefault("to-convert")
+ Map map();
+
+ @WithDefault("to-convert")
+ Map mapInt();
+
+ @WithDefault("one,two,three")
+ Map> mapList();
+
+ @WithDefault("1,2,3")
+ Map> mapListInt();
+
+ class StringValueConverter implements Converter {
+ @Override
+ public String convert(String value) throws IllegalArgumentException, NullPointerException {
+ return "converted";
+ }
+ }
+
+ class IntegerValueConverter implements Converter {
+ @Override
+ public Integer convert(String value) throws IllegalArgumentException, NullPointerException {
+ return 999;
+ }
+ }
+
+ enum Numbers {
+ ONE,
+ TWO,
+ THREE
+ }
+
+ class NumbersConverter implements Converter {
+ @Override
+ public Numbers convert(String value) throws IllegalArgumentException, NullPointerException {
+ return Numbers.THREE;
+ }
+ }
+ }
+
+ @Test
+ void converterNotFound() {
+ IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class,
+ () -> forInterface(ConverterNotFound.class).build());
+ assertTrue(illegalArgumentException.getMessage().contains("SRCFG00013"));
+ }
+
+ interface ConverterNotFound {
+ @WithDefault("value")
+ NotFound value();
+
+ class NotFound {
+
+ }
+ }
+
+ @Test
+ void converterNull() {
+ assertThrows(NoSuchElementException.class, () -> forInterface(ConverterNull.class).build());
+ }
+
+ interface ConverterNull {
+ @WithDefault("")
+ String value();
+ }
+}
diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingDefaultsTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingDefaultsTest.java
index 6a78c215c..865c4bbde 100644
--- a/implementation/src/test/java/io/smallrye/config/ConfigMappingDefaultsTest.java
+++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingDefaultsTest.java
@@ -20,6 +20,8 @@
import org.junit.jupiter.api.Test;
import io.smallrye.config.ConfigMappingDefaultsTest.DataSourcesJdbcBuildTimeConfig.DataSourceJdbcOuterNamedBuildTimeConfig;
+import io.smallrye.config.ConfigMappingDefaultsTest.Defaults.Child;
+import io.smallrye.config.ConfigMappingDefaultsTest.Defaults.Parent;
public class ConfigMappingDefaultsTest {
@Test
@@ -212,6 +214,64 @@ void emptyPrefix() {
assertEquals("value", mapping.parent().child().mapNested().get("default").map().get("default"));
}
+ @Test
+ void builder() {
+ Defaults defaults = ConfigInstanceBuilder.forInterface(Defaults.class)
+ .with(Defaults::listNested, List. of())
+ .with(Defaults::parent, ConfigInstanceBuilder.forInterface(Defaults.Parent.class)
+ .with(Defaults.Parent::child, ConfigInstanceBuilder.forInterface(Defaults.Child.class)
+ .with(Defaults.Child::listNested, List. of())
+ .build())
+ .build())
+ .build();
+
+ assertNotNull(defaults);
+ assertEquals("value", defaults.value());
+ assertEquals(10, defaults.primitive());
+ assertTrue(defaults.optional().isPresent());
+ assertEquals("value", defaults.optional().get());
+ assertTrue(defaults.optionalPrimitive().isPresent());
+ assertEquals(10, defaults.optionalPrimitive().getAsInt());
+ assertIterableEquals(List.of("one", "two"), defaults.list());
+ assertEquals("value", defaults.map().get("default"));
+ assertNotNull(defaults.nested());
+ assertEquals("value", defaults.nested().value());
+ assertEquals(10, defaults.nested().primitive());
+ assertTrue(defaults.nested().optional().isPresent());
+ assertEquals("value", defaults.nested().optional().get());
+ assertTrue(defaults.nested().optionalPrimitive().isPresent());
+ assertEquals(10, defaults.nested().optionalPrimitive().getAsInt());
+ assertIterableEquals(List.of("one", "two"), defaults.nested().list());
+ assertEquals("value", defaults.nested().map().get("default"));
+ assertTrue(defaults.optionalNested().isEmpty());
+ assertTrue(defaults.listNested().isEmpty());
+ assertEquals("value", defaults.mapNested().get("default").value());
+ assertEquals(10, defaults.mapNested().get("default").primitive());
+ assertTrue(defaults.mapNested().get("default").optional().isPresent());
+ assertEquals("value", defaults.mapNested().get("default").optional().get());
+ assertTrue(defaults.mapNested().get("default").optionalPrimitive().isPresent());
+ assertEquals(10, defaults.mapNested().get("default").optionalPrimitive().getAsInt());
+ assertIterableEquals(List.of("one", "two"), defaults.mapNested().get("default").list());
+ assertEquals("value", defaults.mapNested().get("default").map().get("default"));
+ assertEquals("value", defaults.parent().child().nested().value());
+ assertEquals(10, defaults.parent().child().nested().primitive());
+ assertTrue(defaults.parent().child().nested().optional().isPresent());
+ assertEquals("value", defaults.parent().child().nested().optional().get());
+ assertTrue(defaults.parent().child().nested().optionalPrimitive().isPresent());
+ assertEquals(10, defaults.parent().child().nested().optionalPrimitive().getAsInt());
+ assertIterableEquals(List.of("one", "two"), defaults.parent().child().nested().list());
+ assertEquals("value", defaults.parent().child().nested().map().get("default"));
+ assertTrue(defaults.parent().child().optionalNested().isEmpty());
+ assertTrue(defaults.parent().child().listNested().isEmpty());
+ assertEquals("value", defaults.parent().child().mapNested().get("default").value());
+ assertEquals(10, defaults.parent().child().mapNested().get("default").primitive());
+ assertTrue(defaults.parent().child().mapNested().get("default").optional().isPresent());
+ assertEquals("value", defaults.parent().child().mapNested().get("default").optional().get());
+ assertTrue(defaults.parent().child().mapNested().get("default").optionalPrimitive().isPresent());
+ assertIterableEquals(List.of("one", "two"), defaults.parent().child().mapNested().get("default").list());
+ assertEquals("value", defaults.parent().child().mapNested().get("default").map().get("default"));
+ }
+
@ConfigMapping(prefix = "defaults")
interface Defaults {
@WithDefault("value")
diff --git a/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java b/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java
index 806b59c43..6ef46e4b4 100644
--- a/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java
+++ b/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java
@@ -15,9 +15,8 @@
import org.junit.jupiter.api.Test;
+import io.smallrye.config.ConfigInstanceBuilderImpl.MapWithDefault;
import io.smallrye.config.ConfigMapping.NamingStrategy;
-import io.smallrye.config.ConfigMappingContext.MapWithDefault;
-import io.smallrye.config.ConfigMappingContext.ObjectCreator;
public class ObjectCreatorTest {
@Test