diff --git a/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java new file mode 100644 index 000000000..04be4d50f --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigInstanceBuilder.java @@ -0,0 +1,276 @@ +package io.smallrye.config; + +import java.io.Serializable; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.ToDoubleFunction; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; + +import org.eclipse.microprofile.config.spi.Converter; + +/** + * A builder which can produce instances of a configuration interface annotated with {@link ConfigMapping}. + *

+ * Objects which are produced by this API will contain values for every property found on the configuration + * interface or its supertypes. + * If no value is given for a property, its default value is used. + * If a required property has no default value, then an exception will be thrown when {@link #build} is called. + * The returned object instance is immutable and has a stable {@code equals} and {@code hashCode} method. + * If the runtime is Java 16 or later, the returned object may be a {@code Record}. + *

+ * To provide a value for a property, use a method reference to indicate which property the value should be associated + * with. For example, + * + *

+    
+
+    @ConfigMapping
+    interface MyProgramConfig {
+        String message();
+        int repeatCount();
+    }
+
+    ConfigInstanceBuilder<MyProgramConfig> builder = ConfigInstanceBuilder.forInterface(MyProgramConfig.class);
+    builder.with(MyProgramConfig::message, "Hello everyone!");
+    builder.with(MyProgramConfig::repeatCount, 42);
+
+    MyProgramConfig config = builder.build();
+    for (int i = 0; i < config.repeatCount(); i ++) {
+        System.out.println(config.message());
+    }
+    
+ * 
+ * + * Configuration interface member types are automatically converted with a {@link Converter}. Global converters are + * registered either by being discovered via the {@link java.util.ServiceLoader} mechanism, and can be + * registered by providing a {@code META-INF/services/org.eclipse.microprofile.config.spi.Converter} file, which + * contains the fully qualified class name of the custom {@code Converter} implementation, or explicitly by calling + * {@link ConfigInstanceBuilder#registerConverter(Class, Converter)}. + *

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