From 3c3bb435094f99cd8a9bcd2fee0064a90d152a70 Mon Sep 17 00:00:00 2001 From: "David M. Lloyd" Date: Tue, 3 Oct 2023 14:48:52 -0500 Subject: [PATCH 1/7] [WIP] API and implementation for config instance builder Fixes #1001 --- .../config/ConfigInstanceBuilder.java | 320 ++++++++++++++ .../config/ConfigInstanceBuilderImpl.java | 418 ++++++++++++++++++ .../config/_private/ConfigMessages.java | 29 ++ 3 files changed, 767 insertions(+) create mode 100644 implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java create mode 100644 implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java 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..2ec0e593b --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java @@ -0,0 +1,320 @@ +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.ToIntFunction; +import java.util.function.ToLongFunction; + +/** + * A builder which can produce instances of a configuration interface. + *

+ * 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, + * + *

+
+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());
+}
+
+ * 
+ * + * @param the configuration interface type + */ +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}) + * @param the accessor type + * @throws IllegalArgumentException if the getter is {@code null} + */ + & Serializable> ConfigInstanceBuilder with(F getter, int 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}) + * @param the accessor type + * @throws IllegalArgumentException if the getter is {@code null} + */ + & Serializable> ConfigInstanceBuilder with(F 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}) + * @param the accessor type + * @throws IllegalArgumentException if the getter is {@code null} + */ + & Serializable> ConfigInstanceBuilder with(F 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}) + * @param the accessor type + * @return this builder (not {@code null}) + * @throws IllegalArgumentException if the getter is {@code null} + */ + default & Serializable> ConfigInstanceBuilder withOptional(F 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}) + * @param the accessor type + * @return this builder (not {@code null}) + * @throws IllegalArgumentException if the getter is {@code null} + */ + default & Serializable> ConfigInstanceBuilder withOptional(F 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}) + * @param the accessor type + * @return this builder (not {@code null}) + * @throws IllegalArgumentException if the getter is {@code null} + */ + default & Serializable> ConfigInstanceBuilder withOptional(F 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(Boolean.valueOf(value))); + } + + /** + * Set a property to its default value (if any). + * + * @param getter the property to modify (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} + */ + & Serializable> ConfigInstanceBuilder withDefaultFor(F getter); + + /** + * Set a property to its default value (if any). + * + * @param getter the property to modify (must not be {@code null}) + * @param the accessor type + * @return this builder (not {@code null}) + * @throws IllegalArgumentException if the getter is {@code null} + */ + & Serializable> ConfigInstanceBuilder withDefaultFor(F getter); + + /** + * Set a property to its default value (if any). + * + * @param getter the property to modify (must not be {@code null}) + * @param the accessor type + * @return this builder (not {@code null}) + * @throws IllegalArgumentException if the getter is {@code null} + */ + & Serializable> ConfigInstanceBuilder withDefaultFor(F getter); + + /** + * Set a property to its default value (if any). + * + * @param getter the property to modify (must not be {@code null}) + * @param the accessor type + * @return this builder (not {@code null}) + * @throws IllegalArgumentException if the getter is {@code null} + */ + & Serializable> ConfigInstanceBuilder withDefaultFor(F getter); + + /** + * Set a property on the configuration object to a string value. + * The value set on the property will be the result of conversion of the string + * using the property's converter. + * + * @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}, + * or if the value is {@code null}, + * or if the value was rejected by the converter + */ + & Serializable> ConfigInstanceBuilder withString(F getter, String value); + + /** + * Set a property on the configuration object to a string value. + * The value set on the property will be the result of conversion of the string + * using the property's converter. + * + * @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}, + * or if the value is {@code null}, + * or if the value was rejected by the converter + */ + & Serializable> ConfigInstanceBuilder withString(F getter, String value); + + /** + * Set a property on the configuration object to a string value. + * The value set on the property will be the result of conversion of the string + * using the property's converter. + * + * @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}, + * or if the value is {@code null}, + * or if the value was rejected by the converter + */ + & Serializable> ConfigInstanceBuilder withString(F getter, String value); + + /** + * Set a property on the configuration object to a string value. + * The value set on the property will be the result of conversion of the string + * using the property's converter. + * + * @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}, + * or if the value is {@code null}, + * or if the value was rejected by the converter + */ + & Serializable> ConfigInstanceBuilder withString(F getter, String value); + + /** + * Set a property on the configuration object to a string value, using the property's + * declaring class and name to identify the property to set. + * The value set on the property will be the result of conversion of the string + * using the property's converter. + * + * @param propertyClass the declaring class of the property to set (must not be {@code null}) + * @param propertyName the name of the property to set (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 property class or name is {@code null}, + * or if the value is {@code null}, + * or if the value was rejected by the converter, + * or if no property matches the given name and declaring class + */ + ConfigInstanceBuilder withString(Class propertyClass, String propertyName, String 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); + } +} 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..503d0a1bb --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java @@ -0,0 +1,418 @@ +package io.smallrye.config; + +import static io.smallrye.config._private.ConfigMessages.msg; + +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.UndeclaredThrowableException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; + +import org.eclipse.microprofile.config.spi.Converter; + +import io.smallrye.common.constraint.Assert; +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(); + String interfaceName = type.getName(); + MethodHandles.Lookup lookup; + try { + lookup = MethodHandles.privateLookupIn(type, myLookup); + } catch (IllegalAccessException e) { + throw msg.accessDenied(getClass(), type); + } + String implInternalName = interfaceName.replace('.', '/') + "$$SC_BuilderImpl"; + Class impl; + try { + impl = lookup.findClass(implInternalName); + } catch (ClassNotFoundException e) { + // generate the impl instead + throw new UnsupportedOperationException("Todo"); + } 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 (ConfigInstanceBuilderImpl) mh.invokeExact(); + } 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<>() { + protected Function computeValue(final Class type) { + assert type.isInterface(); + String interfaceName = type.getName(); + MethodHandles.Lookup lookup; + try { + lookup = MethodHandles.privateLookupIn(type, myLookup); + } catch (IllegalAccessException e) { + throw msg.accessDenied(getClass(), type); + } + String implInternalName = interfaceName.replace('.', '/') + "$$SC_BuilderImpl"; + Class impl; + try { + impl = lookup.findClass(implInternalName); + } catch (ClassNotFoundException e) { + // generate the impl instead + throw new UnsupportedOperationException("Todo"); + } catch (IllegalAccessException e) { + throw msg.accessDenied(getClass(), type); + } + MethodHandle mh; + Class builderClass = null; + if (true) + throw new UnsupportedOperationException("Not finished yet..."); + 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 type.cast(mh.invokeExact(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 & Serializable> ConfigInstanceBuilder with(final F getter, final int value) { + Assert.checkNotNullParam("getter", getter); + Class callerClass = sw.getCallerClass(); + BiConsumer setter = getSetter(getter, callerClass); + setter.accept(builderObject, Integer.valueOf(value)); + return this; + } + + public & Serializable> ConfigInstanceBuilder with(final F getter, + final long value) { + Assert.checkNotNullParam("getter", getter); + Class callerClass = sw.getCallerClass(); + BiConsumer setter = getSetter(getter, callerClass); + setter.accept(builderObject, Long.valueOf(value)); + return this; + } + + public & Serializable> ConfigInstanceBuilder with(final F getter, + final double value) { + Assert.checkNotNullParam("getter", getter); + Class callerClass = sw.getCallerClass(); + BiConsumer setter = getSetter(getter, callerClass); + setter.accept(builderObject, Double.valueOf(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, Boolean.valueOf(value)); + return this; + } + + // ------------------------------------- + + public & Serializable> ConfigInstanceBuilder withDefaultFor(final F getter) { + Assert.checkNotNullParam("getter", getter); + Class callerClass = sw.getCallerClass(); + Consumer resetter = getResetter(getter, callerClass); + resetter.accept(builderObject); + return this; + } + + public & Serializable> ConfigInstanceBuilder withDefaultFor(final F getter) { + Assert.checkNotNullParam("getter", getter); + Class callerClass = sw.getCallerClass(); + Consumer resetter = getResetter(getter, callerClass); + resetter.accept(builderObject); + return this; + } + + public & Serializable> ConfigInstanceBuilder withDefaultFor(final F getter) { + Assert.checkNotNullParam("getter", getter); + Class callerClass = sw.getCallerClass(); + Consumer resetter = getResetter(getter, callerClass); + resetter.accept(builderObject); + return this; + } + + public & Serializable> ConfigInstanceBuilder withDefaultFor(final F getter) { + Assert.checkNotNullParam("getter", getter); + Class callerClass = sw.getCallerClass(); + Consumer resetter = getResetter(getter, callerClass); + resetter.accept(builderObject); + return this; + } + + // ------------------------------------- + + public & Serializable> ConfigInstanceBuilder withString(final F getter, + final String value) { + return withString(getter, value, sw.getCallerClass()); + } + + public & Serializable> ConfigInstanceBuilder withString(final F getter, + final String value) { + return withString(getter, value, sw.getCallerClass()); + } + + public & Serializable> ConfigInstanceBuilder withString(final F getter, + final String value) { + return withString(getter, value, sw.getCallerClass()); + } + + public & Serializable> ConfigInstanceBuilder withString(final F getter, + final String value) { + return withString(getter, value, sw.getCallerClass()); + } + + private ConfigInstanceBuilderImpl withString(final Object getter, final String value, final Class callerClass) { + Assert.checkNotNullParam("getter", getter); + Assert.checkNotNullParam("value", value); + Converter converter = getConverter(getter, callerClass); + BiConsumer setter = getSetter(getter, callerClass); + setter.accept(builderObject, converter.convert(value)); + return this; + } + + // ------------------------------------- + + public ConfigInstanceBuilder withString(final Class propertyClass, final String propertyName, + final String value) { + throw new UnsupportedOperationException("Need class info registry"); + } + + // ------------------------------------- + + public I build() { + return configurationInterface.cast(configFactories.get(configurationInterface).apply(builderObject)); + } + + // ===================================== + + private Converter getConverter(final Object getter, final Class callerClass) { + throw new UnsupportedOperationException("Need class info registry"); + } + + 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.invokeExact(lambda); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable e) { + throw new UndeclaredThrowableException(e); + } + if (!(replaced instanceof SerializedLambda)) { + throw msg.invalidGetter(); + } + SerializedLambda sl = (SerializedLambda) replaced; + if (sl.getCapturedArgCount() != 0) { + throw msg.invalidGetter(); + } + String implClassName = sl.getImplClass(); + // 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, builderClass, 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.invokeExact(builderObject, builder, val); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable e) { + throw new UndeclaredThrowableException(e); + } + }; + } + + private Consumer getResetter(final Object getter, final Class callerClass) { + throw new UnsupportedOperationException("Unsupported for now"); + } + + 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) { + switch (desc.charAt(start)) { + case 'L': { + return parseClassName(desc, start + 1, end - 1); + } + case '[': { + return parseType(desc, start + 1, end).arrayType(); + } + case 'B': { + return byte.class; + } + case 'C': { + return char.class; + } + case 'D': { + return double.class; + } + case 'F': { + return float.class; + } + case 'I': { + return int.class; + } + case 'J': { + return long.class; + } + case 'S': { + return short.class; + } + case 'Z': { + return 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)); + } 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/_private/ConfigMessages.java b/implementation/src/main/java/io/smallrye/config/_private/ConfigMessages.java index 52d54850f..ad0d608e7 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,33 @@ 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); + + 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()); + } + } + + @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(); } From 509499fd6765322371531a37824d5dca23cdb801 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Wed, 3 Sep 2025 21:11:28 +0100 Subject: [PATCH 2/7] ConfigInstanceBuilder implementation: - Builder class generation - Simple leaf types and primitives - Simple optionals - @WithDefault --- .../config/ConfigInstanceBuilder.java | 5 +- .../config/ConfigInstanceBuilderImpl.java | 157 ++++++++----- .../config/ConfigMappingGenerator.java | 157 +++++++++++++ .../config/ConfigMappingInterface.java | 39 ++++ .../smallrye/config/ConfigMappingLoader.java | 48 ++-- .../config/ConfigMappingMetadata.java | 4 + .../java/io/smallrye/config/Converters.java | 17 ++ .../config/ConfigInstanceBuilderTest.java | 218 ++++++++++++++++++ 8 files changed, 569 insertions(+), 76 deletions(-) create mode 100644 implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java diff --git a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java index 2ec0e593b..e603c7be6 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java @@ -7,6 +7,7 @@ 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; @@ -69,7 +70,7 @@ public interface ConfigInstanceBuilder { & Serializable> ConfigInstanceBuilder with(F getter, int value); /** - * Set a property on the configuration object to an integer 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}) @@ -88,7 +89,7 @@ public interface ConfigInstanceBuilder { * @param the accessor type * @throws IllegalArgumentException if the getter is {@code null} */ - & Serializable> ConfigInstanceBuilder with(F getter, double value); + & Serializable> ConfigInstanceBuilder with(F getter, double value); /** * Set a property on the configuration object to a boolean value. diff --git a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java index 503d0a1bb..b3df67d6b 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java @@ -1,20 +1,30 @@ package io.smallrye.config; +import static io.smallrye.config.Converters.newCollectionConverter; 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.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.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.IntFunction; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; import java.util.function.ToIntFunction; import java.util.function.ToLongFunction; @@ -46,20 +56,19 @@ final class ConfigInstanceBuilderImpl implements ConfigInstanceBuilder { private static final ClassValue> builderFactories = new ClassValue<>() { protected Supplier computeValue(final Class type) { assert type.isInterface(); - String interfaceName = type.getName(); + // 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); } - String implInternalName = interfaceName.replace('.', '/') + "$$SC_BuilderImpl"; Class impl; try { - impl = lookup.findClass(implInternalName); + ConfigMappingLoader.ensureLoaded(type); + impl = lookup.findClass(ConfigMappingInterface.ConfigMappingBuilder.getBuilderClassName(type)); } catch (ClassNotFoundException e) { - // generate the impl instead - throw new UnsupportedOperationException("Todo"); + throw new IllegalStateException(e); } catch (IllegalAccessException e) { throw msg.accessDenied(getClass(), type); } @@ -74,7 +83,7 @@ protected Supplier computeValue(final Class type) { // capture the constructor as a Supplier return () -> { try { - return (ConfigInstanceBuilderImpl) mh.invokeExact(); + return mh.invoke(); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { @@ -83,33 +92,33 @@ protected Supplier computeValue(final Class type) { }; } }; + /** * 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(); - String interfaceName = type.getName(); + // 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); } - String implInternalName = interfaceName.replace('.', '/') + "$$SC_BuilderImpl"; Class impl; + Class builderClass; try { - impl = lookup.findClass(implInternalName); + impl = ConfigMappingLoader.ensureLoaded(type).implementation(); + builderClass = lookup.findClass(ConfigMappingInterface.ConfigMappingBuilder.getBuilderClassName(type)); } catch (ClassNotFoundException e) { - // generate the impl instead - throw new UnsupportedOperationException("Todo"); + throw new IllegalStateException(e); } catch (IllegalAccessException e) { throw msg.accessDenied(getClass(), type); } MethodHandle mh; - Class builderClass = null; - if (true) - throw new UnsupportedOperationException("Not finished yet..."); + try { mh = lookup.findConstructor(impl, MethodType.methodType(void.class, builderClass)); } catch (NoSuchMethodException e) { @@ -120,7 +129,7 @@ protected Supplier computeValue(final Class type) { // capture the constructor as a Function return builder -> { try { - return type.cast(mh.invokeExact(builder)); + return mh.invoke(builder); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { @@ -143,7 +152,7 @@ protected Map> computeValue(final Class ty static ConfigInstanceBuilderImpl forInterface(Class configurationInterface) throws IllegalArgumentException, SecurityException { - return new ConfigInstanceBuilderImpl(configurationInterface, builderFactories.get(configurationInterface).get()); + return new ConfigInstanceBuilderImpl<>(configurationInterface, builderFactories.get(configurationInterface).get()); } // ===================================== @@ -196,7 +205,7 @@ public & Serializable> ConfigInstanceBuilde return this; } - public & Serializable> ConfigInstanceBuilder with(final F getter, + public & Serializable> ConfigInstanceBuilder with(final F getter, final double value) { Assert.checkNotNullParam("getter", getter); Class callerClass = sw.getCallerClass(); @@ -293,6 +302,63 @@ public I build() { // ===================================== + public static T convertValue(final String value, final Class type) { + Converter converter = Converters.getConverter(type); + if (converter == null) { + throw new IllegalArgumentException("No converter found for type " + type); + } + return converter.convert(value); + } + + public static > C convertValues( + final String value, + final Class itemType, + final Class collectionType) { + return convertValues(value, itemType, createCollectionFactory(collectionType)); + } + + @SuppressWarnings("unchecked") + public static > C convertValues( + final String value, + final Class itemType, + final IntFunction> collectionFactory) { + Converter converter = Converters.getConverter(itemType); + if (converter == null) { + throw new IllegalArgumentException("No converter found for type " + itemType); + } + return (C) newCollectionConverter(converter, collectionFactory).convert(value); + } + + // TODO - Duplicated from ConfigMappingContext + public static > IntFunction> createCollectionFactory( + final Class type) { + if (type.equals(List.class)) { + return ArrayList::new; + } + + if (type.equals(Set.class)) { + return HashSet::new; + } + + throw new IllegalArgumentException(); + } + + // TODO - Duplicated from ConfigMappingContext + 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 Converter getConverter(final Object getter, final Class callerClass) { throw new UnsupportedOperationException("Need class info registry"); } @@ -313,7 +379,7 @@ private BiConsumer createSetter(Object lambda) { } Object replaced; try { - replaced = writeReplace.invokeExact(lambda); + replaced = writeReplace.invoke(lambda); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { @@ -326,7 +392,6 @@ private BiConsumer createSetter(Object lambda) { if (sl.getCapturedArgCount() != 0) { throw msg.invalidGetter(); } - String implClassName = sl.getImplClass(); // TODO: check implClassName against the supertype hierarchy of the config interface using shared info mapping String setterName = sl.getImplMethodName(); Class type = parseReturnType(sl.getImplMethodSignature()); @@ -337,7 +402,7 @@ private BiConsumer createSetterByName(final String setterName, f Class builderClass = builderObject.getClass(); MethodHandle setter; try { - setter = lookup.findVirtual(builderClass, setterName, MethodType.methodType(void.class, builderClass, type)); + setter = lookup.findVirtual(builderClass, setterName, MethodType.methodType(void.class, type)); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { @@ -347,7 +412,7 @@ private BiConsumer createSetterByName(final String setterName, f MethodHandle castSetter = setter.asType(MethodType.methodType(void.class, builderClass, Object.class)); return (builder, val) -> { try { - castSetter.invokeExact(builderObject, builder, val); + castSetter.invoke(builderObject, val); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { @@ -369,46 +434,24 @@ private Class parseReturnType(final String signature) { } private Class parseType(String desc, int start, int end) { - switch (desc.charAt(start)) { - case 'L': { - return parseClassName(desc, start + 1, end - 1); - } - case '[': { - return parseType(desc, start + 1, end).arrayType(); - } - case 'B': { - return byte.class; - } - case 'C': { - return char.class; - } - case 'D': { - return double.class; - } - case 'F': { - return float.class; - } - case 'I': { - return int.class; - } - case 'J': { - return long.class; - } - case 'S': { - return short.class; - } - case 'Z': { - return boolean.class; - } - default: { - throw msg.invalidGetter(); - } - } + 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)); + return lookup.findClass(signature.substring(start, end).replaceAll("/", ".")); } catch (ClassNotFoundException e) { throw msg.invalidGetter(); } catch (IllegalAccessException e) { diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java index 4984fe9dd..f147401ef 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; @@ -16,9 +17,11 @@ 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 +35,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; @@ -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; @@ -108,6 +114,7 @@ public class ConfigMappingGenerator { private static final String I_OBJECT = getInternalName(Object.class); private static final String I_STRING = getInternalName(String.class); private static final String I_ITERABLE = getInternalName(Iterable.class); + private static final String I_COLLECTION = getInternalName(Collection.class); private static final int V_THIS = 0; private static final int V_MAPPING_CONTEXT = 1; @@ -134,6 +141,24 @@ 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()) { + Method method = property.getMethod(); + String memberName = method.getName(); + String fieldDesc = getDescriptor(method.getReturnType()); + builderCtor.visitVarInsn(ALOAD, V_THIS); + builderCtor.visitVarInsn(ALOAD, 1); + builderCtor.visitFieldInsn(GETFIELD, builderName, memberName, fieldDesc); + 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); @@ -173,6 +198,138 @@ static byte[] generate(final ConfigMappingInterface mapping) { return writer.toByteArray(); } + private static final String I_CONFIG_INSTANCE_BUILDER = getInternalName(ConfigInstanceBuilder.class); + private static final String I_CONFIG_INSTANCE_BUILDER_IMPL = getInternalName(ConfigInstanceBuilderImpl.class); + + static byte[] generateBuilder(final ConfigMappingInterface mapping, final String builderClassName) { + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + ClassVisitor visitor = usefulDebugInfo ? new Debugging.ClassVisitorImpl(writer) : writer; + + visitor.visit(V1_8, ACC_PUBLIC, builderClassName, null, I_OBJECT, new String[] {}); + visitor.visitSource(null, null); + + // No Args Constructor + MethodVisitor noArgsCtor = visitor.visitMethod(ACC_PUBLIC, "", "()V", null, null); + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESPECIAL, I_OBJECT, "", "()V", false); + for (Property property : mapping.getProperties()) { + if ((property.isLeaf() || property.isPrimitive()) && property.hasDefaultValue() + && property.getDefaultValue() != null) { + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitLdcInsn(property.getDefaultValue()); + if (property.isPrimitive()) { + PrimitiveProperty primitive = property.asPrimitive(); + noArgsCtor.visitLdcInsn(Type.getType(getDescriptor(primitive.getBoxType()))); + } else { + noArgsCtor.visitLdcInsn(Type.getType(getDescriptor(property.getMethod().getReturnType()))); + } + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValue", + "(L" + I_STRING + ";L" + I_CLASS + ";)L" + I_OBJECT + ";", false); + if (property.isPrimitive()) { + PrimitiveProperty primitive = property.asPrimitive(); + noArgsCtor.visitTypeInsn(CHECKCAST, getInternalName(primitive.getBoxType())); + noArgsCtor.visitMethodInsn(INVOKEVIRTUAL, getInternalName(primitive.getBoxType()), + primitive.getUnboxMethodName(), + primitive.getUnboxMethodDescriptor(), false); + } else { + noArgsCtor.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType())); + } + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, property.getMethod().getName(), + getDescriptor(property.getMethod().getReturnType())); + } else if (property.isGroup()) { + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitLdcInsn(Type.getType(getDescriptor(property.getMethod().getReturnType()))); + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER, "forInterface", + "(L" + I_CLASS + ";)L" + I_CONFIG_INSTANCE_BUILDER + ";", true); + noArgsCtor.visitMethodInsn(INVOKEINTERFACE, I_CONFIG_INSTANCE_BUILDER, "build", "()L" + I_OBJECT + ";", true); + noArgsCtor.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType())); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, property.getMethod().getName(), + getDescriptor(property.getMethod().getReturnType())); + } else if (property.isCollection() && property.asCollection().getElement().isLeaf()) { + CollectionProperty collectionProperty = property.asCollection(); + LeafProperty elementProperty = collectionProperty.getElement().asLeaf(); + if (elementProperty.hasDefaultValue() && elementProperty.getDefaultValue() != null) { + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitLdcInsn(elementProperty.getDefaultValue()); + noArgsCtor.visitLdcInsn(Type.getType(getDescriptor(elementProperty.getValueRawType()))); + noArgsCtor.visitLdcInsn(Type.getType(getDescriptor(collectionProperty.getCollectionRawType()))); + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValues", + "(L" + I_STRING + ";L" + I_CLASS + ";L" + I_CLASS + ";)L" + I_COLLECTION + ";", false); + noArgsCtor.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType())); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, property.getMethod().getName(), + getDescriptor(property.getMethod().getReturnType())); + } + } else if (property.isMap()) { + MapProperty mapProperty = property.asMap(); + if (mapProperty.getValueProperty().isLeaf() && mapProperty.hasDefaultValue() + && mapProperty.getDefaultValue() != null) { + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitTypeInsn(NEW, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault"); + noArgsCtor.visitInsn(DUP); + noArgsCtor.visitLdcInsn(mapProperty.getDefaultValue()); + noArgsCtor.visitMethodInsn(INVOKESPECIAL, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault", "", + "(L" + I_OBJECT + ";)V", false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, property.getMethod().getName(), + getDescriptor(property.getMethod().getReturnType())); + } else if (mapProperty.getValueProperty().isGroup()) { + GroupProperty groupProperty = mapProperty.getValueProperty().asGroup(); + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitTypeInsn(NEW, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault"); + noArgsCtor.visitInsn(DUP); + noArgsCtor.visitLdcInsn(getType(groupProperty.getGroupType().getInterfaceType())); + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER, "forInterface", + "(L" + I_CLASS + ";)L" + I_CONFIG_INSTANCE_BUILDER + ";", true); + noArgsCtor.visitMethodInsn(INVOKEINTERFACE, I_CONFIG_INSTANCE_BUILDER, "build", "()L" + I_OBJECT + ";", + true); + noArgsCtor.visitTypeInsn(CHECKCAST, getInternalName(groupProperty.getGroupType().getInterfaceType())); + noArgsCtor.visitMethodInsn(INVOKESPECIAL, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault", "", + "(L" + I_OBJECT + ";)V", false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, property.getMethod().getName(), + getDescriptor(property.getMethod().getReturnType())); + } + } + } + noArgsCtor.visitInsn(RETURN); + noArgsCtor.visitEnd(); + noArgsCtor.visitMaxs(0, 0); + + for (Property property : mapping.getProperties()) { + Method method = property.getMethod(); + String memberName = method.getName(); + + // Field Declaration + String fieldDesc = getDescriptor(method.getReturnType()); + // TODO - Should it be public? And use field access to copy from the builder to the config class? + 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, builderClassName, memberName, fieldDesc); + mv.visitInsn(RETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + return writer.toByteArray(); + } + /** * Generates a configuration interface to act as a middle ground between a configuration class and the backing * implementation of a configuration interface. diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java index b4f5f7c71..f70387b7a 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,11 @@ public Property[] getProperties() { return properties; } + // TODO - Document + 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 +203,38 @@ public byte[] getClassBytes() { } } + public class ConfigMappingBuilder implements ConfigMappingMetadata { + private final String builderClassName; + + public 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/Converters.java b/implementation/src/main/java/io/smallrye/config/Converters.java index 2c2ea1a13..481624544 100644 --- a/implementation/src/main/java/io/smallrye/config/Converters.java +++ b/implementation/src/main/java/io/smallrye/config/Converters.java @@ -296,6 +296,23 @@ public static Type getConverterType(Class clazz) { return getConverterType(clazz.getSuperclass()); } + // TODO - Should we add / keep this here? + @SuppressWarnings("unchecked") + public static Converter getConverter(Class type) { + final Converter exactConverter = ALL_CONVERTERS.get(type); + if (exactConverter != null) { + return (Converter) exactConverter; + } + if (type.isPrimitive()) { + return (Converter) getConverter(Converters.wrapPrimitiveType(type)); + } + if (type.isArray()) { + final Converter conv = getConverter(type.getComponentType()); + return conv == null ? null : Converters.newArrayConverter(conv, type); + } + return Implicit.getConverter(type); + } + /** * Get the implicit converter for the given type class, if any. * 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..90a789208 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java @@ -0,0 +1,218 @@ +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +class ConfigInstanceBuilderTest { + @Test + void builder() { + Server server = forInterface(Server.class) + .with(Server::host, "localhost") + .with(Server::port, Integer.valueOf(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((Function & Serializable) Optionals::optionalInt, 1) + .withOptional((Function & Serializable) Optionals::optionalLong, 1) + .withOptional(Optionals::optionalDouble, 1.1d) + .withOptional(Optionals::optionalBoolean, true) + .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()); + } + + @ConfigMapping + interface Optionals { + Optional optional(); + + OptionalInt optionalInt(); + + OptionalLong optionalLong(); + + OptionalDouble optionalDouble(); + + Optional optionalBoolean(); + } + + @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 collections() { + Collections collections = forInterface(Collections.class) + .with(Collections::list, List.of("one", "two", "three")) + .build(); + + assertIterableEquals(List.of("one", "two", "three"), collections.list()); + assertNull(collections.empty()); + assertIterableEquals(List.of("one", "two", "three"), collections.defaults()); + } + + @ConfigMapping + interface Collections { + List list(); + + List empty(); + + @WithDefault("one,two,three") + List defaults(); + } + + @Test + void maps() { + Maps maps = forInterface(Maps.class) + .with(Maps::map, Map.of("one", "one", "two", "two")) + .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("value", maps.group().get("any").value()); + } + + @ConfigMapping + interface Maps { + Map map(); + + Map empty(); + + @WithDefault("value") + Map defaults(); + + @WithDefaults + Map group(); + + interface Group { + @WithDefault("value") + String value(); + } + } +} From 4850d9959779456d4cee31c88ba834108e65b3fe Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Fri, 5 Sep 2025 15:37:28 +0100 Subject: [PATCH 3/7] ConfigInstanceBuilder implementation: - Builder class generation - Leaf types and primitives - Optionals - Maps - Collections - @WithDefault --- .../config/ConfigInstanceBuilder.java | 45 +- .../config/ConfigInstanceBuilderImpl.java | 87 +- .../config/ConfigMappingGenerator.java | 228 +++-- .../java/io/smallrye/config/Converters.java | 17 - .../config/ConfigInstanceBuilderFullTest.java | 905 ++++++++++++++++++ .../config/ConfigInstanceBuilderTest.java | 36 +- 6 files changed, 1209 insertions(+), 109 deletions(-) create mode 100644 implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderFullTest.java diff --git a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java index e603c7be6..130597548 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java @@ -11,6 +11,8 @@ 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. *

@@ -64,10 +66,9 @@ public interface ConfigInstanceBuilder { * @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, int value); + ConfigInstanceBuilder with(ToIntFunctionGetter getter, int value); /** * Set a property on the configuration object to a long value. @@ -75,10 +76,9 @@ public interface ConfigInstanceBuilder { * @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, long value); + ConfigInstanceBuilder with(ToLongFunctionGetter getter, long value); /** * Set a property on the configuration object to a floating-point value. @@ -86,10 +86,9 @@ public interface ConfigInstanceBuilder { * @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, double value); + ConfigInstanceBuilder with(ToDoubleFunctionGetter getter, double value); /** * Set a property on the configuration object to a boolean value. @@ -123,12 +122,10 @@ default > & Serializable> ConfigIns * * @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, - int value) { + default ConfigInstanceBuilder withOptional(OptionalIntGetter getter, int value) { return with(getter, OptionalInt.of(value)); } @@ -137,12 +134,10 @@ default & Serializable> ConfigInstan * * @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, - long value) { + default ConfigInstanceBuilder withOptional(OptionalLongGetter getter, long value) { return with(getter, OptionalLong.of(value)); } @@ -151,12 +146,10 @@ default & Serializable> ConfigInsta * * @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, - double value) { + default ConfigInstanceBuilder withOptional(OptionalDoubleGetter getter, double value) { return with(getter, OptionalDouble.of(value)); } @@ -318,4 +311,26 @@ static ConfigInstanceBuilder forInterface(Class interfaceClass) throws IllegalArgumentException, SecurityException { return ConfigInstanceBuilderImpl.forInterface(interfaceClass); } + + static void registerConverter(Class type, Converter converter) { + ConfigInstanceBuilderImpl.CONVERTERS.put(type, converter); + } + + interface ToIntFunctionGetter extends ToIntFunction, Serializable { + } + + interface ToLongFunctionGetter extends ToLongFunction, Serializable { + } + + interface ToDoubleFunctionGetter extends ToDoubleFunction, Serializable { + } + + interface OptionalIntGetter extends Function, Serializable { + } + + interface OptionalLongGetter extends Function, Serializable { + } + + 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 index b3df67d6b..ccd037db4 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java @@ -1,6 +1,7 @@ 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; @@ -9,6 +10,7 @@ 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; @@ -16,6 +18,9 @@ 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; @@ -24,13 +29,15 @@ import java.util.function.IntFunction; import java.util.function.Predicate; import java.util.function.Supplier; -import java.util.function.ToDoubleFunction; import java.util.function.ToIntFunction; import java.util.function.ToLongFunction; 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; /** @@ -188,7 +195,7 @@ public & Serializable> ConfigInstanceBuilde return this; } - public & Serializable> ConfigInstanceBuilder with(final F getter, final int value) { + public ConfigInstanceBuilder with(final ToIntFunctionGetter getter, final int value) { Assert.checkNotNullParam("getter", getter); Class callerClass = sw.getCallerClass(); BiConsumer setter = getSetter(getter, callerClass); @@ -196,8 +203,7 @@ public & Serializable> ConfigInstanceBuilder return this; } - public & Serializable> ConfigInstanceBuilder with(final F getter, - final long value) { + public ConfigInstanceBuilder with(final ToLongFunctionGetter getter, final long value) { Assert.checkNotNullParam("getter", getter); Class callerClass = sw.getCallerClass(); BiConsumer setter = getSetter(getter, callerClass); @@ -205,12 +211,11 @@ public & Serializable> ConfigInstanceBuilde return this; } - public & Serializable> ConfigInstanceBuilder with(final F getter, - final double value) { + 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, Double.valueOf(value)); + setter.accept(builderObject, value); return this; } @@ -302,14 +307,64 @@ public I build() { // ===================================== + 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") + private 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()) { + final Converter conv = getConverter(type.getComponentType()); + return conv == null ? null : Converters.newArrayConverter(conv, type); + } + + return Implicit.getConverter(type); + } + public static T convertValue(final String value, final Class type) { - Converter converter = Converters.getConverter(type); + Converter converter = getConverter(type); if (converter == null) { throw new IllegalArgumentException("No converter found for type " + type); } return converter.convert(value); } + public static Optional convertOptionalValue(final String value, final Class type) { + Converter converter = getConverter(type); + if (converter == null) { + throw new IllegalArgumentException("No converter found for type " + type); + } + return Converters.newOptionalConverter(converter).convert(value); + } + public static > C convertValues( final String value, final Class itemType, @@ -317,12 +372,24 @@ public static > C convertValues( return convertValues(value, itemType, createCollectionFactory(collectionType)); } + @SuppressWarnings("unchecked") + public > Optional convertOptionalValues( + final String value, + final Class itemType, + final IntFunction> collectionFactory) { + Converter converter = getConverter(itemType); + if (converter == null) { + throw new IllegalArgumentException("No converter found for type " + itemType); + } + return (Optional) newOptionalConverter(newCollectionConverter(converter, collectionFactory)).convert(value); + } + @SuppressWarnings("unchecked") public static > C convertValues( final String value, final Class itemType, final IntFunction> collectionFactory) { - Converter converter = Converters.getConverter(itemType); + Converter converter = getConverter(itemType); if (converter == null) { throw new IllegalArgumentException("No converter found for type " + itemType); } @@ -330,7 +397,7 @@ public static > C convertValues( } // TODO - Duplicated from ConfigMappingContext - public static > IntFunction> createCollectionFactory( + static > IntFunction> createCollectionFactory( final Class type) { if (type.equals(List.class)) { return ArrayList::new; diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java index f147401ef..a3653e473 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java @@ -67,6 +67,9 @@ 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; @@ -113,8 +116,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; @@ -213,82 +218,181 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String noArgsCtor.visitVarInsn(ALOAD, V_THIS); noArgsCtor.visitMethodInsn(INVOKESPECIAL, I_OBJECT, "", "()V", false); for (Property property : mapping.getProperties()) { - if ((property.isLeaf() || property.isPrimitive()) && property.hasDefaultValue() - && property.getDefaultValue() != null) { - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitLdcInsn(property.getDefaultValue()); - if (property.isPrimitive()) { - PrimitiveProperty primitive = property.asPrimitive(); - noArgsCtor.visitLdcInsn(Type.getType(getDescriptor(primitive.getBoxType()))); - } else { - noArgsCtor.visitLdcInsn(Type.getType(getDescriptor(property.getMethod().getReturnType()))); + String fieldDesc = getDescriptor(property.getMethod().getReturnType()); + String memberName = property.getMethod().getName(); + if (property.isLeaf() && !property.isOptional() || property.isPrimitive()) { + if (property.hasDefaultValue() && property.getDefaultValue() != null) { + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, memberName, "()" + fieldDesc, false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); + + MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, memberName, "()" + fieldDesc, null, null); + mv.visitLdcInsn(property.getDefaultValue()); + if (property.isPrimitive()) { + PrimitiveProperty primitive = property.asPrimitive(); + mv.visitLdcInsn(Type.getType(getDescriptor(primitive.getBoxType()))); + } else { + mv.visitLdcInsn(Type.getType(getDescriptor(property.getMethod().getReturnType()))); + } + mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValue", + "(L" + I_STRING + ";L" + I_CLASS + ";)L" + I_OBJECT + ";", false); + if (property.isPrimitive()) { + PrimitiveProperty primitive = property.asPrimitive(); + mv.visitTypeInsn(CHECKCAST, getInternalName(primitive.getBoxType())); + mv.visitMethodInsn(INVOKEVIRTUAL, getInternalName(primitive.getBoxType()), + primitive.getUnboxMethodName(), + primitive.getUnboxMethodDescriptor(), false); + } else { + mv.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType())); + } + mv.visitInsn(getReturnInstruction(property)); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } else if (property.isLeaf()) { + LeafProperty leafProperty = property.asLeaf(); + if (leafProperty.getValueRawType().equals(OptionalInt.class)) { + String I_OPTIONAL_INT = getInternalName(OptionalInt.class); + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_INT, "empty", "()L" + I_OPTIONAL_INT + ";", false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL_INT + ";"); + } else if (leafProperty.getValueRawType().equals(OptionalLong.class)) { + String I_OPTIONAL_LONG = getInternalName(OptionalLong.class); + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_LONG, "empty", "()L" + I_OPTIONAL_LONG + ";", + false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL_LONG + ";"); + } else if (leafProperty.getValueRawType().equals(OptionalDouble.class)) { + String I_OPTIONAL_DOUBLE = getInternalName(OptionalDouble.class); + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_DOUBLE, "empty", "()L" + I_OPTIONAL_DOUBLE + ";", + false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL_DOUBLE + ";"); + } } - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValue", - "(L" + I_STRING + ";L" + I_CLASS + ";)L" + I_OBJECT + ";", false); - if (property.isPrimitive()) { - PrimitiveProperty primitive = property.asPrimitive(); - noArgsCtor.visitTypeInsn(CHECKCAST, getInternalName(primitive.getBoxType())); - noArgsCtor.visitMethodInsn(INVOKEVIRTUAL, getInternalName(primitive.getBoxType()), - primitive.getUnboxMethodName(), - primitive.getUnboxMethodDescriptor(), false); + } else if (property.isOptional() && property.isLeaf()) { + if (property.hasDefaultValue() && property.getDefaultValue() != null) { + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, memberName, "()" + fieldDesc, false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); + + MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, memberName, "()" + fieldDesc, null, null); + LeafProperty optionalProperty = property.asLeaf(); + mv.visitLdcInsn(property.getDefaultValue()); + mv.visitLdcInsn(Type.getType(getDescriptor(optionalProperty.getValueRawType()))); + mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertOptionalValue", + "(L" + I_STRING + ";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 { - noArgsCtor.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType())); + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL + ";"); } - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, property.getMethod().getName(), - getDescriptor(property.getMethod().getReturnType())); - } else if (property.isGroup()) { - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitLdcInsn(Type.getType(getDescriptor(property.getMethod().getReturnType()))); - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER, "forInterface", - "(L" + I_CLASS + ";)L" + I_CONFIG_INSTANCE_BUILDER + ";", true); - noArgsCtor.visitMethodInsn(INVOKEINTERFACE, I_CONFIG_INSTANCE_BUILDER, "build", "()L" + I_OBJECT + ";", true); - noArgsCtor.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType())); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, property.getMethod().getName(), - getDescriptor(property.getMethod().getReturnType())); + } else if (property.isMap()) { + MapProperty mapProperty = property.asMap(); + if (mapProperty.getValueProperty().isLeaf()) { + if (mapProperty.hasDefaultValue() && mapProperty.getDefaultValue() != null) { + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, memberName, "()" + fieldDesc, false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); + + MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, memberName, "()" + fieldDesc, null, + null); + mv.visitTypeInsn(NEW, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault"); + mv.visitInsn(DUP); + mv.visitLdcInsn(mapProperty.getDefaultValue()); + 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 { + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_MAP + ";"); + } + } else if (mapProperty.getValueProperty().isCollection() + && mapProperty.getValueProperty().asCollection().getElement().isLeaf()) { + // TODO - Check if Collections in Maps support defaults (can't remember) + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_MAP + ";"); + } else if (mapProperty.getValueProperty().isGroup()) + if (mapProperty.hasDefaultValue()) { + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, memberName, "()" + fieldDesc, false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); + + MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, memberName, "()" + fieldDesc, null, + null); + GroupProperty groupProperty = mapProperty.getValueProperty().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 { + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, 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) { noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitLdcInsn(elementProperty.getDefaultValue()); - noArgsCtor.visitLdcInsn(Type.getType(getDescriptor(elementProperty.getValueRawType()))); - noArgsCtor.visitLdcInsn(Type.getType(getDescriptor(collectionProperty.getCollectionRawType()))); - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValues", + noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, memberName, "()" + fieldDesc, false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); + + MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, memberName, "()" + fieldDesc, null, null); + mv.visitLdcInsn(elementProperty.getDefaultValue()); + mv.visitLdcInsn(Type.getType(getDescriptor(elementProperty.getValueRawType()))); + mv.visitLdcInsn(Type.getType(getDescriptor(collectionProperty.getCollectionRawType()))); + mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValues", "(L" + I_STRING + ";L" + I_CLASS + ";L" + I_CLASS + ";)L" + I_COLLECTION + ";", false); - noArgsCtor.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType())); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, property.getMethod().getName(), - getDescriptor(property.getMethod().getReturnType())); + mv.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType())); + mv.visitInsn(getReturnInstruction(property)); + mv.visitMaxs(0, 0); + mv.visitEnd(); } - } else if (property.isMap()) { - MapProperty mapProperty = property.asMap(); - if (mapProperty.getValueProperty().isLeaf() && mapProperty.hasDefaultValue() - && mapProperty.getDefaultValue() != null) { - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitTypeInsn(NEW, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault"); - noArgsCtor.visitInsn(DUP); - noArgsCtor.visitLdcInsn(mapProperty.getDefaultValue()); - noArgsCtor.visitMethodInsn(INVOKESPECIAL, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault", "", - "(L" + I_OBJECT + ";)V", false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, property.getMethod().getName(), - getDescriptor(property.getMethod().getReturnType())); - } else if (mapProperty.getValueProperty().isGroup()) { - GroupProperty groupProperty = mapProperty.getValueProperty().asGroup(); + } 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) { + throw new UnsupportedOperationException(); + } else { noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitTypeInsn(NEW, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault"); - noArgsCtor.visitInsn(DUP); - noArgsCtor.visitLdcInsn(getType(groupProperty.getGroupType().getInterfaceType())); - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER, "forInterface", - "(L" + I_CLASS + ";)L" + I_CONFIG_INSTANCE_BUILDER + ";", true); - noArgsCtor.visitMethodInsn(INVOKEINTERFACE, I_CONFIG_INSTANCE_BUILDER, "build", "()L" + I_OBJECT + ";", - true); - noArgsCtor.visitTypeInsn(CHECKCAST, getInternalName(groupProperty.getGroupType().getInterfaceType())); - noArgsCtor.visitMethodInsn(INVOKESPECIAL, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault", "", - "(L" + I_OBJECT + ";)V", false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, property.getMethod().getName(), - getDescriptor(property.getMethod().getReturnType())); + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL + ";"); } + } else if (property.isGroup()) { + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, memberName, "()" + fieldDesc, false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); + + MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, memberName, "()" + fieldDesc, null, null); + mv.visitLdcInsn(Type.getType(getDescriptor(property.getMethod().getReturnType()))); + 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(); } } + // TODO - May required recursive lookups noArgsCtor.visitInsn(RETURN); noArgsCtor.visitEnd(); noArgsCtor.visitMaxs(0, 0); diff --git a/implementation/src/main/java/io/smallrye/config/Converters.java b/implementation/src/main/java/io/smallrye/config/Converters.java index 481624544..2c2ea1a13 100644 --- a/implementation/src/main/java/io/smallrye/config/Converters.java +++ b/implementation/src/main/java/io/smallrye/config/Converters.java @@ -296,23 +296,6 @@ public static Type getConverterType(Class clazz) { return getConverterType(clazz.getSuperclass()); } - // TODO - Should we add / keep this here? - @SuppressWarnings("unchecked") - public static Converter getConverter(Class type) { - final Converter exactConverter = ALL_CONVERTERS.get(type); - if (exactConverter != null) { - return (Converter) exactConverter; - } - if (type.isPrimitive()) { - return (Converter) getConverter(Converters.wrapPrimitiveType(type)); - } - if (type.isArray()) { - final Converter conv = getConverter(type.getComponentType()); - return conv == null ? null : Converters.newArrayConverter(conv, type); - } - return Implicit.getConverter(type); - } - /** * Get the implicit converter for the given type class, if any. * 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..08a71c0a1 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderFullTest.java @@ -0,0 +1,905 @@ +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.assertNull; +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) + .build(); + + assertEquals(8080, httpConfig.port()); + assertEquals(8081, httpConfig.testPort()); + assertNull(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()); + } + + @Test + void populate() { + HttpConfig httpConfig = ConfigInstanceBuilder.forInterface(HttpConfig.class) + .build(); + } + + @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 index 90a789208..f11cfa0b2 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java @@ -6,14 +6,12 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.Serializable; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalDouble; import java.util.OptionalInt; import java.util.OptionalLong; -import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -22,7 +20,7 @@ class ConfigInstanceBuilderTest { void builder() { Server server = forInterface(Server.class) .with(Server::host, "localhost") - .with(Server::port, Integer.valueOf(8080)) + .with(Server::port, 8080) .build(); assertEquals("localhost", server.host()); @@ -108,8 +106,8 @@ interface Group { void optionals() { Optionals optionals = forInterface(Optionals.class) .withOptional(Optionals::optional, "value") - .withOptional((Function & Serializable) Optionals::optionalInt, 1) - .withOptional((Function & Serializable) Optionals::optionalLong, 1) + .withOptional(Optionals::optionalInt, 1) + .withOptional(Optionals::optionalLong, 1) .withOptional(Optionals::optionalDouble, 1.1d) .withOptional(Optionals::optionalBoolean, true) .build(); @@ -163,6 +161,34 @@ interface Nested { } } + @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()); + } + + interface OptionalDefaults { + @WithDefault("value") + Optional optional(); + + @WithDefault("10") + OptionalInt optionalInt(); + + @WithDefault("10") + OptionalLong optionalLong(); + + @WithDefault("10.10") + OptionalDouble optionalDouble(); + } + @Test void collections() { Collections collections = forInterface(Collections.class) From e761fc47b3d68d5aa4c1fd31125cba9ef2cdcbce Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Thu, 11 Sep 2025 17:08:20 +0100 Subject: [PATCH 4/7] ConfigInstanceBuilder throw exception if a required value is not set --- .../config/ConfigInstanceBuilderImpl.java | 8 + .../config/ConfigMappingGenerator.java | 168 +++++++++++------- .../config/_private/ConfigMessages.java | 3 + .../config/ConfigInstanceBuilderFullTest.java | 10 +- .../config/ConfigInstanceBuilderTest.java | 10 +- 5 files changed, 125 insertions(+), 74 deletions(-) diff --git a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java index ccd037db4..3dba71afa 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java @@ -396,6 +396,14 @@ public static > C convertValues( return (C) newCollectionConverter(converter, collectionFactory).convert(value); } + public static T requireValue(final T value, final String name) { + if (value == null) { + // TODO - Change message? + throw msg.propertyNotSet(name); + } + return value; + } + // TODO - Duplicated from ConfigMappingContext static > IntFunction> createCollectionFactory( final Class type) { diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java index a3653e473..8eb4c0c88 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java @@ -157,7 +157,13 @@ static byte[] generate(final ConfigMappingInterface mapping) { String fieldDesc = getDescriptor(method.getReturnType()); builderCtor.visitVarInsn(ALOAD, V_THIS); builderCtor.visitVarInsn(ALOAD, 1); - builderCtor.visitFieldInsn(GETFIELD, builderName, memberName, fieldDesc); + 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); } builderCtor.visitInsn(RETURN); @@ -205,6 +211,9 @@ static byte[] generate(final ConfigMappingInterface mapping) { 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); static byte[] generateBuilder(final ConfigMappingInterface mapping, final String builderClassName) { ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); @@ -220,49 +229,56 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String for (Property property : mapping.getProperties()) { String fieldDesc = getDescriptor(property.getMethod().getReturnType()); String memberName = property.getMethod().getName(); - if (property.isLeaf() && !property.isOptional() || property.isPrimitive()) { + 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 + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, defaultMethodName, "()" + fieldDesc, false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); + // Default Method + PrimitiveProperty primitive = property.asPrimitive(); + MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc, null, + null); + mv.visitLdcInsn(property.getDefaultValue()); + mv.visitLdcInsn(Type.getType(getDescriptor(primitive.getBoxType()))); + mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValue", + "(L" + I_STRING + ";L" + I_CLASS + ";)L" + I_OBJECT + ";", false); + mv.visitTypeInsn(CHECKCAST, getInternalName(primitive.getBoxType())); + mv.visitMethodInsn(INVOKEVIRTUAL, + getInternalName(primitive.getBoxType()), + primitive.getUnboxMethodName(), + primitive.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) { - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, memberName, "()" + fieldDesc, false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); - - MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, memberName, "()" + fieldDesc, null, null); + generateGetterWithDefaullt = true; + // Default Method + MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc, null, + null); mv.visitLdcInsn(property.getDefaultValue()); - if (property.isPrimitive()) { - PrimitiveProperty primitive = property.asPrimitive(); - mv.visitLdcInsn(Type.getType(getDescriptor(primitive.getBoxType()))); - } else { - mv.visitLdcInsn(Type.getType(getDescriptor(property.getMethod().getReturnType()))); - } + mv.visitLdcInsn(Type.getType(fieldDesc)); mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValue", "(L" + I_STRING + ";L" + I_CLASS + ";)L" + I_OBJECT + ";", false); - if (property.isPrimitive()) { - PrimitiveProperty primitive = property.asPrimitive(); - mv.visitTypeInsn(CHECKCAST, getInternalName(primitive.getBoxType())); - mv.visitMethodInsn(INVOKEVIRTUAL, getInternalName(primitive.getBoxType()), - primitive.getUnboxMethodName(), - primitive.getUnboxMethodDescriptor(), false); - } else { - mv.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType())); - } + mv.visitTypeInsn(CHECKCAST, getInternalName(property.getMethod().getReturnType())); mv.visitInsn(getReturnInstruction(property)); mv.visitMaxs(0, 0); mv.visitEnd(); - } else if (property.isLeaf()) { - LeafProperty leafProperty = property.asLeaf(); + } else { + // There is no default, but we initialize empty Optionals inline in field if (leafProperty.getValueRawType().equals(OptionalInt.class)) { - String I_OPTIONAL_INT = getInternalName(OptionalInt.class); noArgsCtor.visitVarInsn(ALOAD, V_THIS); noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_INT, "empty", "()L" + I_OPTIONAL_INT + ";", false); noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL_INT + ";"); } else if (leafProperty.getValueRawType().equals(OptionalLong.class)) { - String I_OPTIONAL_LONG = getInternalName(OptionalLong.class); noArgsCtor.visitVarInsn(ALOAD, V_THIS); noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_LONG, "empty", "()L" + I_OPTIONAL_LONG + ";", false); noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL_LONG + ";"); } else if (leafProperty.getValueRawType().equals(OptionalDouble.class)) { - String I_OPTIONAL_DOUBLE = getInternalName(OptionalDouble.class); noArgsCtor.visitVarInsn(ALOAD, V_THIS); noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_DOUBLE, "empty", "()L" + I_OPTIONAL_DOUBLE + ";", false); @@ -271,11 +287,10 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String } } else if (property.isOptional() && property.isLeaf()) { if (property.hasDefaultValue() && property.getDefaultValue() != null) { - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, memberName, "()" + fieldDesc, false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); - - MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, memberName, "()" + fieldDesc, null, 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()); mv.visitLdcInsn(Type.getType(getDescriptor(optionalProperty.getValueRawType()))); @@ -286,6 +301,7 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitMaxs(0, 0); mv.visitEnd(); } else { + // There is no default, but we initialize an empty Optional inline in field noArgsCtor.visitVarInsn(ALOAD, V_THIS); noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false); noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL + ";"); @@ -294,11 +310,10 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String MapProperty mapProperty = property.asMap(); if (mapProperty.getValueProperty().isLeaf()) { if (mapProperty.hasDefaultValue() && mapProperty.getDefaultValue() != null) { - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, memberName, "()" + fieldDesc, false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); - - MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, memberName, "()" + fieldDesc, 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); @@ -309,6 +324,7 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitMaxs(0, 0); mv.visitEnd(); } else { + // There is no default, but we initialize an empty Map inline in field noArgsCtor.visitVarInsn(ALOAD, V_THIS); noArgsCtor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true); noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_MAP + ";"); @@ -316,16 +332,16 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String } else if (mapProperty.getValueProperty().isCollection() && mapProperty.getValueProperty().asCollection().getElement().isLeaf()) { // TODO - Check if Collections in Maps support defaults (can't remember) + // There is no default, but we initialize an empty Map inline in field noArgsCtor.visitVarInsn(ALOAD, V_THIS); noArgsCtor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true); noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_MAP + ";"); } else if (mapProperty.getValueProperty().isGroup()) if (mapProperty.hasDefaultValue()) { - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, memberName, "()" + fieldDesc, false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); - - MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, memberName, "()" + fieldDesc, null, + generateGetterWithDefaullt = true; + // Default Method + MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc, + null, null); GroupProperty groupProperty = mapProperty.getValueProperty().asGroup(); mv.visitTypeInsn(NEW, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault"); @@ -342,6 +358,7 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitMaxs(0, 0); mv.visitEnd(); } else { + // There is no default, but we initialize an empty Map inline in field noArgsCtor.visitVarInsn(ALOAD, V_THIS); noArgsCtor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true); noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_MAP + ";"); @@ -350,11 +367,10 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String CollectionProperty collectionProperty = property.asCollection(); LeafProperty elementProperty = collectionProperty.getElement().asLeaf(); if (elementProperty.hasDefaultValue() && elementProperty.getDefaultValue() != null) { - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, memberName, "()" + fieldDesc, false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); - - MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, memberName, "()" + fieldDesc, null, null); + generateGetterWithDefaullt = true; + // Default Method + MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc, null, + null); mv.visitLdcInsn(elementProperty.getDefaultValue()); mv.visitLdcInsn(Type.getType(getDescriptor(elementProperty.getValueRawType()))); mv.visitLdcInsn(Type.getType(getDescriptor(collectionProperty.getCollectionRawType()))); @@ -372,17 +388,17 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String if (elementProperty.hasDefaultValue() && elementProperty.getDefaultValue() != null) { throw new UnsupportedOperationException(); } else { + // There is no default, but we initialize an empty Optional inline in field noArgsCtor.visitVarInsn(ALOAD, V_THIS); noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false); noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL + ";"); } } else if (property.isGroup()) { - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, memberName, "()" + fieldDesc, false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); - - MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, memberName, "()" + fieldDesc, null, null); - mv.visitLdcInsn(Type.getType(getDescriptor(property.getMethod().getReturnType()))); + 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); @@ -391,6 +407,30 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitMaxs(0, 0); mv.visitEnd(); } + + MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC, memberName, "()" + fieldDesc, null, null); + if (generateGetterWithDefaullt) { + mv.visitVarInsn(ALOAD, V_THIS); + mv.visitFieldInsn(GETFIELD, builderClassName, memberName, fieldDesc); + Label _ifNull = new Label(); + mv.visitJumpInsn(IFNULL, _ifNull); + mv.visitVarInsn(ALOAD, V_THIS); + mv.visitFieldInsn(GETFIELD, builderClassName, memberName, fieldDesc); + mv.visitInsn(ARETURN); + mv.visitLabel(_ifNull); + mv.visitFrame(F_SAME, 0, null, 0, null); + mv.visitMethodInsn(INVOKESTATIC, builderClassName, defaultMethodName, "()" + fieldDesc, false); + mv.visitInsn(ARETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } else { + mv.visitVarInsn(ALOAD, V_THIS); + mv.visitFieldInsn(GETFIELD, builderClassName, memberName, fieldDesc); + mv.visitInsn(getReturnInstruction(property)); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + } // TODO - May required recursive lookups noArgsCtor.visitInsn(RETURN); @@ -407,28 +447,28 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String 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); + MethodVisitor mvs = visitor.visitMethod(ACC_PUBLIC, memberName, "(" + fieldDesc + ")V", null, null); + mvs.visitVarInsn(ALOAD, V_THIS); switch (Type.getReturnType(method).getSort()) { case Type.BOOLEAN, Type.SHORT, Type.CHAR, Type.BYTE, Type.INT -> - mv.visitVarInsn(ILOAD, 1); + mvs.visitVarInsn(ILOAD, 1); - case Type.LONG -> mv.visitVarInsn(LLOAD, 1); + case Type.LONG -> mvs.visitVarInsn(LLOAD, 1); - case Type.FLOAT -> mv.visitVarInsn(FLOAD, 1); + case Type.FLOAT -> mvs.visitVarInsn(FLOAD, 1); - case Type.DOUBLE -> mv.visitVarInsn(DLOAD, 1); + case Type.DOUBLE -> mvs.visitVarInsn(DLOAD, 1); - default -> mv.visitVarInsn(ALOAD, 1); + default -> mvs.visitVarInsn(ALOAD, 1); } - mv.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); - mv.visitInsn(RETURN); - mv.visitMaxs(0, 0); - mv.visitEnd(); + mvs.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); + mvs.visitInsn(RETURN); + mvs.visitMaxs(0, 0); + mvs.visitEnd(); } return writer.toByteArray(); 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 ad0d608e7..2b50bd234 100644 --- a/implementation/src/main/java/io/smallrye/config/_private/ConfigMessages.java +++ b/implementation/src/main/java/io/smallrye/config/_private/ConfigMessages.java @@ -202,4 +202,7 @@ default SecurityException accessDenied(Class ourClass, Class targetType) { @Message(id = 56, value = "The accessor for a configuration property is not valid") IllegalArgumentException invalidGetter(); + + @Message(id = 56, value = "The property %s is required but it was not set in the ConfigInstanceBuilder") + NoSuchElementException propertyNotSet(String property); } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderFullTest.java b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderFullTest.java index 08a71c0a1..fd6a0832e 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderFullTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderFullTest.java @@ -6,7 +6,6 @@ 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.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.Serial; @@ -51,11 +50,12 @@ static void beforeAll() { @Test void emptyWithDefaults() { HttpConfig httpConfig = ConfigInstanceBuilder.forInterface(HttpConfig.class) + .with(HttpConfig::host, "localhost") .build(); assertEquals(8080, httpConfig.port()); assertEquals(8081, httpConfig.testPort()); - assertNull(httpConfig.host()); + assertEquals("localhost", httpConfig.host()); assertFalse(httpConfig.testHost().isPresent()); assertTrue(httpConfig.hostEnabled()); assertEquals(8443, httpConfig.sslPort()); @@ -232,12 +232,6 @@ void emptyWithDefaults() { assertFalse(httpConfig.websocketServer().maxMessageSize().isPresent()); } - @Test - void populate() { - HttpConfig httpConfig = ConfigInstanceBuilder.forInterface(HttpConfig.class) - .build(); - } - @ConfigMapping interface HttpConfig { AuthRuntimeConfig auth(); diff --git a/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java index f11cfa0b2..29b4affd3 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java @@ -3,11 +3,13 @@ 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.assertNull; +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; @@ -192,12 +194,16 @@ interface OptionalDefaults { @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()); - assertNull(collections.empty()); + assertNotNull(collections.empty()); + assertTrue(collections.empty().isEmpty()); assertIterableEquals(List.of("one", "two", "three"), collections.defaults()); + + assertThrows(NoSuchElementException.class, () -> forInterface(Collections.class).build()); } @ConfigMapping From 7b97bdc548d2ec7ca5c4a383afc373d6b04ddfa6 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Thu, 11 Sep 2025 23:53:43 +0100 Subject: [PATCH 5/7] ConfigInstanceBuilder missing default cases: - Optional Group - Map with Collections - Default methods --- .../config/ConfigInstanceBuilderImpl.java | 1 - .../config/ConfigMappingGenerator.java | 92 ++++++++++++++----- .../config/ConfigInstanceBuilderTest.java | 32 +++++++ .../config/ConfigMappingDefaultsTest.java | 60 ++++++++++++ 4 files changed, 163 insertions(+), 22 deletions(-) diff --git a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java index 3dba71afa..a9d6db064 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java @@ -398,7 +398,6 @@ public static > C convertValues( public static T requireValue(final T value, final String name) { if (value == null) { - // TODO - Change message? throw msg.propertyNotSet(name); } return value; diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java index 8eb4c0c88..9c356590a 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java @@ -151,21 +151,41 @@ static byte[] generate(final ConfigMappingInterface mapping) { 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()); - 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())); + + 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.visitFieldInsn(PUTFIELD, mapping.getClassInternalName(), memberName, fieldDesc); } + builderCtor.visitInsn(RETURN); builderCtor.visitEnd(); builderCtor.visitMaxs(0, 0); @@ -227,6 +247,10 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String noArgsCtor.visitVarInsn(ALOAD, V_THIS); noArgsCtor.visitMethodInsn(INVOKESPECIAL, I_OBJECT, "", "()V", false); for (Property property : mapping.getProperties()) { + if (property.isDefaultMethod()) { + continue; + } + String fieldDesc = getDescriptor(property.getMethod().getReturnType()); String memberName = property.getMethod().getName(); String defaultMethodName = "default_" + memberName; @@ -308,7 +332,8 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String } } else if (property.isMap()) { MapProperty mapProperty = property.asMap(); - if (mapProperty.getValueProperty().isLeaf()) { + Property valueProperty = mapProperty.getValueProperty(); + if (valueProperty.isLeaf()) { if (mapProperty.hasDefaultValue() && mapProperty.getDefaultValue() != null) { generateGetterWithDefaullt = true; // Default Method @@ -318,6 +343,7 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitTypeInsn(NEW, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault"); mv.visitInsn(DUP); mv.visitLdcInsn(mapProperty.getDefaultValue()); + // TODO - Miss Converter mv.visitMethodInsn(INVOKESPECIAL, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault", "", "(L" + I_OBJECT + ";)V", false); mv.visitInsn(getReturnInstruction(property)); @@ -329,21 +355,41 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String noArgsCtor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true); noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_MAP + ";"); } - } else if (mapProperty.getValueProperty().isCollection() - && mapProperty.getValueProperty().asCollection().getElement().isLeaf()) { - // TODO - Check if Collections in Maps support defaults (can't remember) - // There is no default, but we initialize an empty Map inline in field - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_MAP + ";"); - } else if (mapProperty.getValueProperty().isGroup()) + } 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()); + mv.visitLdcInsn(Type.getType(getDescriptor(elementProperty.getValueRawType()))); + mv.visitLdcInsn(Type.getType(getDescriptor(collectionProperty.getCollectionRawType()))); + mv.visitMethodInsn(INVOKESTATIC, I_CONFIG_INSTANCE_BUILDER_IMPL, "convertValues", + "(L" + I_STRING + ";L" + I_CLASS + ";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 + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, 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 = mapProperty.getValueProperty().asGroup(); + GroupProperty groupProperty = valueProperty.asGroup(); mv.visitTypeInsn(NEW, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault"); mv.visitInsn(DUP); mv.visitLdcInsn(getType(groupProperty.getGroupType().getInterfaceType())); @@ -406,6 +452,11 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String 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 + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false); + noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL + ";"); } MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC, memberName, "()" + fieldDesc, null, null); @@ -430,9 +481,8 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitMaxs(0, 0); mv.visitEnd(); } - } - // TODO - May required recursive lookups + noArgsCtor.visitInsn(RETURN); noArgsCtor.visitEnd(); noArgsCtor.visitMaxs(0, 0); diff --git a/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java index 29b4affd3..42db8b773 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java @@ -112,6 +112,9 @@ void optionals() { .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()); @@ -124,6 +127,16 @@ void optionals() { 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 @@ -137,6 +150,12 @@ interface Optionals { OptionalDouble optionalDouble(); Optional optionalBoolean(); + + Optional group(); + + interface Group { + String value(); + } } @Test @@ -220,6 +239,7 @@ interface Collections { 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")); @@ -228,6 +248,11 @@ void maps() { assertEquals("value", maps.defaults().get("two")); assertEquals("value", maps.defaults().get("three")); assertEquals("value", maps.group().get("any").value()); + assertIterableEquals(List.of("one", "two", "three"), maps.mapLists().get("any")); + + assertThrows(NoSuchElementException.class, () -> forInterface(Maps.class) + .with(Maps::map, Map.of("one", "one", "two", "two")) + .build()); } @ConfigMapping @@ -242,6 +267,13 @@ interface Maps { @WithDefaults Map group(); + // TODO - Add defaults for middle maps? + @WithDefault("any") + Map> nested(); + + @WithDefault("one,two,three") + Map> mapLists(); + interface Group { @WithDefault("value") 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") From c3222f2c92c28550205abcb40667690e9fbfd894 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Fri, 12 Sep 2025 13:42:55 +0100 Subject: [PATCH 6/7] ConfigInstanceBuilder support @WithConverter --- .../config/ConfigInstanceBuilderImpl.java | 63 ++++---- .../config/ConfigMappingGenerator.java | 116 +++++++++++--- .../config/ConfigInstanceBuilderTest.java | 146 ++++++++++++++++++ 3 files changed, 271 insertions(+), 54 deletions(-) diff --git a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java index a9d6db064..f5f260014 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java @@ -333,7 +333,7 @@ private static void registerConverters() { } @SuppressWarnings("unchecked") - private static Converter getConverter(Class type) { + public static Converter getConverter(Class type) { Converter exactConverter = CONVERTERS.get(type); if (exactConverter != null) { return (Converter) exactConverter; @@ -342,58 +342,49 @@ private static Converter getConverter(Class type) { return (Converter) getConverter(Converters.wrapPrimitiveType(type)); } if (type.isArray()) { - final Converter conv = getConverter(type.getComponentType()); - return conv == null ? null : Converters.newArrayConverter(conv, type); + Converter conv = getConverter(type.getComponentType()); + if (conv != null) { + return Converters.newArrayConverter(conv, type); + } + throw ConfigMessages.msg.noRegisteredConverter(type); } - return Implicit.getConverter(type); - } - - public static T convertValue(final String value, final Class type) { - Converter converter = getConverter(type); + Converter converter = Implicit.getConverter(type); if (converter == null) { - throw new IllegalArgumentException("No converter found for type " + type); + throw ConfigMessages.msg.noRegisteredConverter(type); } - return converter.convert(value); + return converter; } - public static Optional convertOptionalValue(final String value, final Class type) { - Converter converter = getConverter(type); - if (converter == null) { - throw new IllegalArgumentException("No converter found for type " + type); + 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 Converters.newOptionalConverter(converter).convert(value); + return convert; } - public static > C convertValues( - final String value, - final Class itemType, - final Class collectionType) { - return convertValues(value, itemType, createCollectionFactory(collectionType)); + public static Optional convertOptionalValue(final String value, final Converter converter) { + return convertValue(value, Converters.newOptionalConverter(converter)); } @SuppressWarnings("unchecked") - public > Optional convertOptionalValues( + public static > C convertValues( final String value, - final Class itemType, - final IntFunction> collectionFactory) { - Converter converter = getConverter(itemType); - if (converter == null) { - throw new IllegalArgumentException("No converter found for type " + itemType); - } - return (Optional) newOptionalConverter(newCollectionConverter(converter, collectionFactory)).convert(value); + final Converter converter, + final Class collectionType) { + return (C) convertValue(value, newCollectionConverter(converter, createCollectionFactory(collectionType))); } @SuppressWarnings("unchecked") - public static > C convertValues( + public static > Optional convertOptionalValues( final String value, - final Class itemType, - final IntFunction> collectionFactory) { - Converter converter = getConverter(itemType); - if (converter == null) { - throw new IllegalArgumentException("No converter found for type " + itemType); - } - return (C) newCollectionConverter(converter, collectionFactory).convert(value); + final Converter converter, + final Class collectionType) { + Converter> collectionConverter = newCollectionConverter(converter, + createCollectionFactory(collectionType)); + return (Optional) newOptionalConverter(collectionConverter).convert(value); } public static T requireValue(final T value, final String name) { diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java index 9c356590a..be4498166 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java @@ -74,6 +74,7 @@ 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; @@ -234,6 +235,7 @@ static byte[] generate(final ConfigMappingInterface mapping) { 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 builderClassName) { ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); @@ -251,6 +253,7 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String continue; } + // Set Default / Generate method to retrieve the default String fieldDesc = getDescriptor(property.getMethod().getReturnType()); String memberName = property.getMethod().getName(); String defaultMethodName = "default_" + memberName; @@ -261,18 +264,27 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, defaultMethodName, "()" + fieldDesc, false); noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); // Default Method - PrimitiveProperty primitive = property.asPrimitive(); + PrimitiveProperty primitiveProperty = property.asPrimitive(); MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc, null, null); mv.visitLdcInsn(property.getDefaultValue()); - mv.visitLdcInsn(Type.getType(getDescriptor(primitive.getBoxType()))); + 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_CLASS + ";)L" + I_OBJECT + ";", false); - mv.visitTypeInsn(CHECKCAST, getInternalName(primitive.getBoxType())); + "(L" + I_STRING + ";L" + I_CONVERTER + ";)L" + I_OBJECT + ";", false); + mv.visitTypeInsn(CHECKCAST, getInternalName(primitiveProperty.getBoxType())); mv.visitMethodInsn(INVOKEVIRTUAL, - getInternalName(primitive.getBoxType()), - primitive.getUnboxMethodName(), - primitive.getUnboxMethodDescriptor(), false); + getInternalName(primitiveProperty.getBoxType()), + primitiveProperty.getUnboxMethodName(), + primitiveProperty.getUnboxMethodDescriptor(), false); mv.visitInsn(getReturnInstruction(property)); mv.visitMaxs(0, 0); mv.visitEnd(); @@ -284,9 +296,18 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc, null, null); mv.visitLdcInsn(property.getDefaultValue()); - mv.visitLdcInsn(Type.getType(fieldDesc)); + 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_CLASS + ";)L" + I_OBJECT + ";", false); + "(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); @@ -317,9 +338,18 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String null); LeafProperty optionalProperty = property.asLeaf(); mv.visitLdcInsn(property.getDefaultValue()); - mv.visitLdcInsn(Type.getType(getDescriptor(optionalProperty.getValueRawType()))); + 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_CLASS + ";)L" + I_OPTIONAL + ";", false); + "(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); @@ -343,7 +373,18 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitTypeInsn(NEW, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault"); mv.visitInsn(DUP); mv.visitLdcInsn(mapProperty.getDefaultValue()); - // TODO - Miss Converter + 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)); @@ -367,10 +408,19 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitTypeInsn(NEW, I_CONFIG_INSTANCE_BUILDER_IMPL + "$MapWithDefault"); mv.visitInsn(DUP); mv.visitLdcInsn(mapProperty.getDefaultValue()); - mv.visitLdcInsn(Type.getType(getDescriptor(elementProperty.getValueRawType()))); + 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_CLASS + ";L" + I_CLASS + ";)L" + I_COLLECTION + ";", false); + "(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)); @@ -418,10 +468,19 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC | ACC_STATIC, defaultMethodName, "()" + fieldDesc, null, null); mv.visitLdcInsn(elementProperty.getDefaultValue()); - mv.visitLdcInsn(Type.getType(getDescriptor(elementProperty.getValueRawType()))); + 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_CLASS + ";L" + I_CLASS + ";)L" + I_COLLECTION + ";", false); + "(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); @@ -432,7 +491,28 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String CollectionProperty collectionProperty = property.asOptional().getNestedProperty().asCollection(); LeafProperty elementProperty = collectionProperty.getElement().asLeaf(); if (elementProperty.hasDefaultValue() && elementProperty.getDefaultValue() != null) { - throw new UnsupportedOperationException(); + 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 noArgsCtor.visitVarInsn(ALOAD, V_THIS); @@ -459,6 +539,7 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL + ";"); } + // Getter MethodVisitor mv = visitor.visitMethod(ACC_PUBLIC, memberName, "()" + fieldDesc, null, null); if (generateGetterWithDefaullt) { mv.visitVarInsn(ALOAD, V_THIS); @@ -493,7 +574,6 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String // Field Declaration String fieldDesc = getDescriptor(method.getReturnType()); - // TODO - Should it be public? And use field access to copy from the builder to the config class? visitor.visitField(ACC_PUBLIC, memberName, fieldDesc, null, null); // Setter diff --git a/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java index 42db8b773..21d4605cc 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigInstanceBuilderTest.java @@ -14,9 +14,14 @@ 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() { @@ -194,6 +199,8 @@ void optionalDefaults() { 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 { @@ -208,6 +215,9 @@ interface OptionalDefaults { @WithDefault("10.10") OptionalDouble optionalDouble(); + + @WithDefault("one,two,three") + Optional> optionalList(); } @Test @@ -221,6 +231,7 @@ void collections() { 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()); } @@ -233,6 +244,9 @@ interface Collections { @WithDefault("one,two,three") List defaults(); + + @WithDefault("one") + Set setDefaults(); } @Test @@ -247,8 +261,10 @@ void maps() { 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")) @@ -264,6 +280,9 @@ interface Maps { @WithDefault("value") Map defaults(); + @WithDefault("10") + Map mapIntegers(); + @WithDefaults Map group(); @@ -274,9 +293,136 @@ interface Maps { @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(); + } } From bdd9fd982095e34ed0af49f5f3e6db85109ab227 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Fri, 12 Sep 2025 15:30:08 +0100 Subject: [PATCH 7/7] ConfigInstanceBuilder cleanups: - Remove unused methods from ConfigInstanceBuilder - Share the same code between ConfigInstanceBuilder and ConfigMappingContext - Simplify ConfigMappingGenerator --- .../config/ConfigInstanceBuilder.java | 202 +++++-------- .../config/ConfigInstanceBuilderImpl.java | 107 +------ .../smallrye/config/ConfigMappingContext.java | 18 +- .../config/ConfigMappingGenerator.java | 270 ++++-------------- .../config/ConfigMappingInterface.java | 13 +- .../config/_private/ConfigMessages.java | 24 +- .../io/smallrye/config/ObjectCreatorTest.java | 3 +- 7 files changed, 163 insertions(+), 474 deletions(-) diff --git a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java index 130597548..04be4d50f 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java @@ -14,7 +14,7 @@ import org.eclipse.microprofile.config.spi.Converter; /** - * A builder which can produce instances of a configuration interface. + * 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. @@ -24,22 +24,42 @@ * 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, + * with. For example, * *

-
-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());
-}
-
+    
+
+    @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 { /** @@ -164,127 +184,9 @@ default ConfigInstanceBuilder withOptional(OptionalDoubleGetter getter, do */ default > & Serializable> ConfigInstanceBuilder withOptional(F getter, boolean value) { - return with(getter, Optional.of(Boolean.valueOf(value))); + return with(getter, Optional.of(value)); } - /** - * Set a property to its default value (if any). - * - * @param getter the property to modify (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} - */ - & Serializable> ConfigInstanceBuilder withDefaultFor(F getter); - - /** - * Set a property to its default value (if any). - * - * @param getter the property to modify (must not be {@code null}) - * @param the accessor type - * @return this builder (not {@code null}) - * @throws IllegalArgumentException if the getter is {@code null} - */ - & Serializable> ConfigInstanceBuilder withDefaultFor(F getter); - - /** - * Set a property to its default value (if any). - * - * @param getter the property to modify (must not be {@code null}) - * @param the accessor type - * @return this builder (not {@code null}) - * @throws IllegalArgumentException if the getter is {@code null} - */ - & Serializable> ConfigInstanceBuilder withDefaultFor(F getter); - - /** - * Set a property to its default value (if any). - * - * @param getter the property to modify (must not be {@code null}) - * @param the accessor type - * @return this builder (not {@code null}) - * @throws IllegalArgumentException if the getter is {@code null} - */ - & Serializable> ConfigInstanceBuilder withDefaultFor(F getter); - - /** - * Set a property on the configuration object to a string value. - * The value set on the property will be the result of conversion of the string - * using the property's converter. - * - * @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}, - * or if the value is {@code null}, - * or if the value was rejected by the converter - */ - & Serializable> ConfigInstanceBuilder withString(F getter, String value); - - /** - * Set a property on the configuration object to a string value. - * The value set on the property will be the result of conversion of the string - * using the property's converter. - * - * @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}, - * or if the value is {@code null}, - * or if the value was rejected by the converter - */ - & Serializable> ConfigInstanceBuilder withString(F getter, String value); - - /** - * Set a property on the configuration object to a string value. - * The value set on the property will be the result of conversion of the string - * using the property's converter. - * - * @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}, - * or if the value is {@code null}, - * or if the value was rejected by the converter - */ - & Serializable> ConfigInstanceBuilder withString(F getter, String value); - - /** - * Set a property on the configuration object to a string value. - * The value set on the property will be the result of conversion of the string - * using the property's converter. - * - * @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}, - * or if the value is {@code null}, - * or if the value was rejected by the converter - */ - & Serializable> ConfigInstanceBuilder withString(F getter, String value); - - /** - * Set a property on the configuration object to a string value, using the property's - * declaring class and name to identify the property to set. - * The value set on the property will be the result of conversion of the string - * using the property's converter. - * - * @param propertyClass the declaring class of the property to set (must not be {@code null}) - * @param propertyName the name of the property to set (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 property class or name is {@code null}, - * or if the value is {@code null}, - * or if the value was rejected by the converter, - * or if no property matches the given name and declaring class - */ - ConfigInstanceBuilder withString(Class propertyClass, String propertyName, String value); - /** * Build the configuration instance. * @@ -312,25 +214,63 @@ static ConfigInstanceBuilder forInterface(Class interfaceClass) 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 index f5f260014..3b9f2423c 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilderImpl.java @@ -24,13 +24,10 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.IntFunction; import java.util.function.Predicate; import java.util.function.Supplier; -import java.util.function.ToIntFunction; -import java.util.function.ToLongFunction; import org.eclipse.microprofile.config.spi.Converter; @@ -107,7 +104,6 @@ protected Supplier computeValue(final Class type) { // 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(); - // TODO - Should we cache this eagerly in io.smallrye.config.ConfigMappingLoader.ConfigMappingImplementation? MethodHandles.Lookup lookup; try { lookup = MethodHandles.privateLookupIn(type, myLookup); @@ -199,7 +195,7 @@ public ConfigInstanceBuilder with(final ToIntFunctionGetter getter, final Assert.checkNotNullParam("getter", getter); Class callerClass = sw.getCallerClass(); BiConsumer setter = getSetter(getter, callerClass); - setter.accept(builderObject, Integer.valueOf(value)); + setter.accept(builderObject, value); return this; } @@ -207,7 +203,7 @@ public ConfigInstanceBuilder with(final ToLongFunctionGetter getter, final Assert.checkNotNullParam("getter", getter); Class callerClass = sw.getCallerClass(); BiConsumer setter = getSetter(getter, callerClass); - setter.accept(builderObject, Long.valueOf(value)); + setter.accept(builderObject, value); return this; } @@ -223,84 +219,10 @@ public & Serializable> ConfigInstanceBuilder Assert.checkNotNullParam("getter", getter); Class callerClass = sw.getCallerClass(); BiConsumer setter = getSetter(getter, callerClass); - setter.accept(builderObject, Boolean.valueOf(value)); - return this; - } - - // ------------------------------------- - - public & Serializable> ConfigInstanceBuilder withDefaultFor(final F getter) { - Assert.checkNotNullParam("getter", getter); - Class callerClass = sw.getCallerClass(); - Consumer resetter = getResetter(getter, callerClass); - resetter.accept(builderObject); - return this; - } - - public & Serializable> ConfigInstanceBuilder withDefaultFor(final F getter) { - Assert.checkNotNullParam("getter", getter); - Class callerClass = sw.getCallerClass(); - Consumer resetter = getResetter(getter, callerClass); - resetter.accept(builderObject); - return this; - } - - public & Serializable> ConfigInstanceBuilder withDefaultFor(final F getter) { - Assert.checkNotNullParam("getter", getter); - Class callerClass = sw.getCallerClass(); - Consumer resetter = getResetter(getter, callerClass); - resetter.accept(builderObject); - return this; - } - - public & Serializable> ConfigInstanceBuilder withDefaultFor(final F getter) { - Assert.checkNotNullParam("getter", getter); - Class callerClass = sw.getCallerClass(); - Consumer resetter = getResetter(getter, callerClass); - resetter.accept(builderObject); - return this; - } - - // ------------------------------------- - - public & Serializable> ConfigInstanceBuilder withString(final F getter, - final String value) { - return withString(getter, value, sw.getCallerClass()); - } - - public & Serializable> ConfigInstanceBuilder withString(final F getter, - final String value) { - return withString(getter, value, sw.getCallerClass()); - } - - public & Serializable> ConfigInstanceBuilder withString(final F getter, - final String value) { - return withString(getter, value, sw.getCallerClass()); - } - - public & Serializable> ConfigInstanceBuilder withString(final F getter, - final String value) { - return withString(getter, value, sw.getCallerClass()); - } - - private ConfigInstanceBuilderImpl withString(final Object getter, final String value, final Class callerClass) { - Assert.checkNotNullParam("getter", getter); - Assert.checkNotNullParam("value", value); - Converter converter = getConverter(getter, callerClass); - BiConsumer setter = getSetter(getter, callerClass); - setter.accept(builderObject, converter.convert(value)); + setter.accept(builderObject, value); return this; } - // ------------------------------------- - - public ConfigInstanceBuilder withString(final Class propertyClass, final String propertyName, - final String value) { - throw new UnsupportedOperationException("Need class info registry"); - } - - // ------------------------------------- - public I build() { return configurationInterface.cast(configFactories.get(configurationInterface).apply(builderObject)); } @@ -365,11 +287,12 @@ public static T convertValue(final String value, final Converter converte return convert; } + @SuppressWarnings("unused") public static Optional convertOptionalValue(final String value, final Converter converter) { return convertValue(value, Converters.newOptionalConverter(converter)); } - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "unused" }) public static > C convertValues( final String value, final Converter converter, @@ -377,7 +300,7 @@ public static > C convertValues( return (C) convertValue(value, newCollectionConverter(converter, createCollectionFactory(collectionType))); } - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "unused" }) public static > Optional convertOptionalValues( final String value, final Converter converter, @@ -387,6 +310,7 @@ public static > Optional convertOptionalValues( 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); @@ -394,8 +318,7 @@ public static T requireValue(final T value, final String name) { return value; } - // TODO - Duplicated from ConfigMappingContext - static > IntFunction> createCollectionFactory( + public static > IntFunction> createCollectionFactory( final Class type) { if (type.equals(List.class)) { return ArrayList::new; @@ -408,8 +331,7 @@ static > IntFunction> createC throw new IllegalArgumentException(); } - // TODO - Duplicated from ConfigMappingContext - static class MapWithDefault extends HashMap { + public static class MapWithDefault extends HashMap { @Serial private static final long serialVersionUID = 1390928078837140814L; private final V defaultValue; @@ -424,10 +346,6 @@ public V get(final Object key) { } } - private Converter getConverter(final Object getter, final Class callerClass) { - throw new UnsupportedOperationException("Need class info registry"); - } - private BiConsumer getSetter(final Object getter, final Class callerClass) { Map> setterMap = setterMapsByCallingClass.get(callerClass); BiConsumer setter = setterMap.get(getter); @@ -450,10 +368,9 @@ private BiConsumer createSetter(Object lambda) { } catch (Throwable e) { throw new UndeclaredThrowableException(e); } - if (!(replaced instanceof SerializedLambda)) { + if (!(replaced instanceof SerializedLambda sl)) { throw msg.invalidGetter(); } - SerializedLambda sl = (SerializedLambda) replaced; if (sl.getCapturedArgCount() != 0) { throw msg.invalidGetter(); } @@ -486,10 +403,6 @@ private BiConsumer createSetterByName(final String setterName, f }; } - private Consumer getResetter(final Object getter, final Class callerClass) { - throw new UnsupportedOperationException("Unsupported for now"); - } - private Class parseReturnType(final String signature) { int idx = signature.lastIndexOf(')'); if (idx == -1) { 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 be4498166..92f4f3497 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java @@ -12,7 +12,6 @@ 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; @@ -60,8 +59,6 @@ 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; @@ -78,7 +75,6 @@ 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; @@ -95,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); @@ -132,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); @@ -227,7 +215,7 @@ 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); @@ -237,17 +225,15 @@ static byte[] generate(final ConfigMappingInterface mapping) { 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 builderClassName) { - ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); - ClassVisitor visitor = usefulDebugInfo ? new Debugging.ClassVisitorImpl(writer) : writer; - - visitor.visit(V1_8, ACC_PUBLIC, builderClassName, null, I_OBJECT, new String[] {}); + 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 noArgsCtor = visitor.visitMethod(ACC_PUBLIC, "", "()V", null, null); - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESPECIAL, I_OBJECT, "", "()V", false); + 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; @@ -260,9 +246,9 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String 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 - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, builderClassName, defaultMethodName, "()" + fieldDesc, false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); + 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, @@ -315,19 +301,19 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String } else { // There is no default, but we initialize empty Optionals inline in field if (leafProperty.getValueRawType().equals(OptionalInt.class)) { - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_INT, "empty", "()L" + I_OPTIONAL_INT + ";", false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL_INT + ";"); + 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)) { - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_LONG, "empty", "()L" + I_OPTIONAL_LONG + ";", + ctor.visitVarInsn(ALOAD, V_THIS); + ctor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_LONG, "empty", "()L" + I_OPTIONAL_LONG + ";", false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL_LONG + ";"); + ctor.visitFieldInsn(PUTFIELD, className, memberName, "L" + I_OPTIONAL_LONG + ";"); } else if (leafProperty.getValueRawType().equals(OptionalDouble.class)) { - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_DOUBLE, "empty", "()L" + I_OPTIONAL_DOUBLE + ";", + ctor.visitVarInsn(ALOAD, V_THIS); + ctor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL_DOUBLE, "empty", "()L" + I_OPTIONAL_DOUBLE + ";", false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL_DOUBLE + ";"); + ctor.visitFieldInsn(PUTFIELD, className, memberName, "L" + I_OPTIONAL_DOUBLE + ";"); } } } else if (property.isOptional() && property.isLeaf()) { @@ -356,9 +342,9 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitEnd(); } else { // There is no default, but we initialize an empty Optional inline in field - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL + ";"); + 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(); @@ -392,9 +378,9 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitEnd(); } else { // There is no default, but we initialize an empty Map inline in field - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_MAP + ";"); + 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(); @@ -428,9 +414,9 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitEnd(); } else { // There is no default, but we initialize an empty Map inline in field - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_MAP + ";"); + 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()) { @@ -455,9 +441,9 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitEnd(); } else { // There is no default, but we initialize an empty Map inline in field - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_MAP, "of", "()L" + I_MAP + ";", true); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_MAP + ";"); + 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(); @@ -515,9 +501,9 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitEnd(); } else { // There is no default, but we initialize an empty Optional inline in field - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL + ";"); + 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; @@ -534,39 +520,39 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String mv.visitEnd(); } else if (property.isOptional() && property.asOptional().getNestedProperty().isGroup()) { // There is no default, but we initialize an empty Optional inline in field - noArgsCtor.visitVarInsn(ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false); - noArgsCtor.visitFieldInsn(PUTFIELD, builderClassName, memberName, "L" + I_OPTIONAL + ";"); + 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, builderClassName, memberName, fieldDesc); + mv.visitFieldInsn(GETFIELD, className, memberName, fieldDesc); Label _ifNull = new Label(); mv.visitJumpInsn(IFNULL, _ifNull); mv.visitVarInsn(ALOAD, V_THIS); - mv.visitFieldInsn(GETFIELD, builderClassName, memberName, fieldDesc); + mv.visitFieldInsn(GETFIELD, className, memberName, fieldDesc); mv.visitInsn(ARETURN); mv.visitLabel(_ifNull); mv.visitFrame(F_SAME, 0, null, 0, null); - mv.visitMethodInsn(INVOKESTATIC, builderClassName, defaultMethodName, "()" + fieldDesc, false); + 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, builderClassName, memberName, fieldDesc); + mv.visitFieldInsn(GETFIELD, className, memberName, fieldDesc); mv.visitInsn(getReturnInstruction(property)); mv.visitMaxs(0, 0); mv.visitEnd(); } } - noArgsCtor.visitInsn(RETURN); - noArgsCtor.visitEnd(); - noArgsCtor.visitMaxs(0, 0); + ctor.visitInsn(RETURN); + ctor.visitEnd(); + ctor.visitMaxs(0, 0); for (Property property : mapping.getProperties()) { Method method = property.getMethod(); @@ -577,31 +563,31 @@ static byte[] generateBuilder(final ConfigMappingInterface mapping, final String visitor.visitField(ACC_PUBLIC, memberName, fieldDesc, null, null); // Setter - MethodVisitor mvs = visitor.visitMethod(ACC_PUBLIC, memberName, "(" + fieldDesc + ")V", null, null); - mvs.visitVarInsn(ALOAD, V_THIS); + 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 -> - mvs.visitVarInsn(ILOAD, 1); + mv.visitVarInsn(ILOAD, 1); - case Type.LONG -> mvs.visitVarInsn(LLOAD, 1); + case Type.LONG -> mv.visitVarInsn(LLOAD, 1); - case Type.FLOAT -> mvs.visitVarInsn(FLOAD, 1); + case Type.FLOAT -> mv.visitVarInsn(FLOAD, 1); - case Type.DOUBLE -> mvs.visitVarInsn(DLOAD, 1); + case Type.DOUBLE -> mv.visitVarInsn(DLOAD, 1); - default -> mvs.visitVarInsn(ALOAD, 1); + default -> mv.visitVarInsn(ALOAD, 1); } - mvs.visitFieldInsn(PUTFIELD, builderClassName, memberName, fieldDesc); - mvs.visitInsn(RETURN); - mvs.visitMaxs(0, 0); - mvs.visitEnd(); + mv.visitFieldInsn(PUTFIELD, className, memberName, fieldDesc); + mv.visitInsn(RETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); } - return writer.toByteArray(); + return visitor.toByteArray(); } /** @@ -1724,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 f70387b7a..69958c1ac 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java @@ -132,7 +132,14 @@ public Property[] getProperties() { return properties; } - // TODO - Document + /** + * 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; } @@ -203,10 +210,10 @@ public byte[] getClassBytes() { } } - public class ConfigMappingBuilder implements ConfigMappingMetadata { + class ConfigMappingBuilder implements ConfigMappingMetadata { private final String builderClassName; - public ConfigMappingBuilder() { + ConfigMappingBuilder() { this.builderClassName = getBuilderClassName(ConfigMappingInterface.this.interfaceType); } 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 2b50bd234..7a2b1adcc 100644 --- a/implementation/src/main/java/io/smallrye/config/_private/ConfigMessages.java +++ b/implementation/src/main/java/io/smallrye/config/_private/ConfigMessages.java @@ -177,17 +177,6 @@ IllegalArgumentException converterException(@Cause Throwable converterException, @Message(id = 52, value = "Could not generate ConfigMapping") IllegalStateException couldNotGenerateMapping(@Cause Throwable throwable); - 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()); - } - } - @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.") @@ -203,6 +192,17 @@ default SecurityException accessDenied(Class ourClass, Class targetType) { @Message(id = 56, value = "The accessor for a configuration property is not valid") IllegalArgumentException invalidGetter(); - @Message(id = 56, value = "The property %s is required but it was not set in the ConfigInstanceBuilder") + @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/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