diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a217b347e..82167e472 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,3 +5,8 @@ updates: schedule: interval: daily open-pull-requests-limit: 10 +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/project.yml b/.github/project.yml index c823288e1..fb7d5a385 100644 --- a/.github/project.yml +++ b/.github/project.yml @@ -1,4 +1,4 @@ name: SmallRye Config release: - current-version: 2.11.1 - next-version: 2.11.2-SNAPSHOT + current-version: 3.7.1 + next-version: 3.7.2-SNAPSHOT diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c5f5a0767..69b9d79ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,8 +4,6 @@ on: push: branches: - main - - 1.x - - jakarta paths-ignore: - '.gitignore' - 'CODEOWNERS' @@ -25,42 +23,48 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [8, 11, 17] + java: [11, 17, 21] name: build with jdk ${{matrix.java}} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 name: checkout - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v4 name: set up jdk ${{matrix.java}} with: + distribution: 'temurin' java-version: ${{matrix.java}} + cache: 'maven' + cache-dependency-path: '**/pom.xml' - name: build with maven run: mvn -B formatter:validate verify --file pom.xml - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 name: tck-report with: - name: tck-report + name: tck-report-java-${{matrix.java}} path: testsuite/tck/target/surefire-reports build-windows: runs-on: windows-latest strategy: matrix: - java: [8, 11, 17] + java: [11, 17, 21] name: build with jdk ${{matrix.java}} windows steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 name: checkout - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v4 name: set up jdk ${{matrix.java}} with: + distribution: 'temurin' java-version: ${{matrix.java}} + cache: 'maven' + cache-dependency-path: '**/pom.xml' - name: build with maven run: mvn -B formatter:validate verify --file pom.xml @@ -72,14 +76,17 @@ jobs: name: docs steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 name: checkout - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v4 with: + distribution: 'temurin' java-version: 11 + cache: 'maven' + cache-dependency-path: '**/pom.xml' - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v5 with: python-version: '3.9' @@ -103,15 +110,24 @@ jobs: name: quality steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-java@v1 + + - uses: actions/setup-java@v4 with: + distribution: 'temurin' java-version: 11 + cache: 'maven' + cache-dependency-path: '**/pom.xml' + + - name: build with docs and coverage + run: mvn verify -Pcoverage javadoc:javadoc + + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 - name: sonar - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - SONAR_TOKEN: ${{secrets.SONAR_TOKEN}} - run: mvn -B verify --file pom.xml -Pcoverage javadoc:javadoc sonar:sonar -Dsonar.projectKey=smallrye_smallrye-config -Dsonar.login=$SONAR_TOKEN + run: mvn sonar:sonar -Dsonar.projectKey=smallrye_smallrye-config -Dsonar.token=${{secrets.SONAR_TOKEN}} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e196dd41..6bc50c5b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,15 +22,16 @@ jobs: github-token: ${{secrets.GITHUB_TOKEN}} metadata-file-path: '.github/project.yml' - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: token: ${{secrets.RELEASE_TOKEN}} - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v4 with: + distribution: 'temurin' java-version: 11 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v5 with: python-version: '3.9' @@ -68,11 +69,12 @@ jobs: with: github-token: ${{secrets.GITHUB_TOKEN}} milestone-title: ${{steps.metadata.outputs.current-version}} + milestone-next: ${{steps.metadata.outputs.next-version}} - name: generate tck report run: | cd target/checkout - mvn surefire-report:report-only + mvn surefire-report:report mv testsuite/tck/target/tck-results.html $GITHUB_WORKSPACE - uses: meeDamian/github-release@2.0 diff --git a/.github/workflows/update-milestone.yml b/.github/workflows/update-milestone.yml new file mode 100644 index 000000000..5a1f33549 --- /dev/null +++ b/.github/workflows/update-milestone.yml @@ -0,0 +1,17 @@ +name: Update Milestone + +on: + pull_request_target: + types: [closed] + +jobs: + update: + runs-on: ubuntu-latest + name: update-milestone + if: ${{github.event.pull_request.merged == true}} + + steps: + - uses: radcortez/milestone-set-action@main + name: milestone set + with: + github-token: ${{secrets.GITHUB_TOKEN}} diff --git a/README.adoc b/README.adoc index 58e160ddd..6251c4f30 100644 --- a/README.adoc +++ b/README.adoc @@ -9,7 +9,14 @@ image:https://img.shields.io/maven-central/v/io.smallrye.config/smallrye-config? = SmallRye Config -SmallRye Config is an implementation of {microprofile-config}[Eclipse MicroProfile Config]. +*SmallRye Config* is a library that provides a way to configure applications, frameworks and containers. It is used +in applications servers like https://wildfly.org/[WildFly] and https://openliberty.io[Open Liberty], or frameworks +like https://quarkus.io[Quarkus]. It can also be used completely standalone in any Java application, which makes it a +very flexible library. + +It follows the https://github.com/eclipse/microprofile-config/[MicroProfile Config] specification to provide +the initial config foundations and expands with it own concepts to cover a wide range of use cases observed in the +configuration space. == Instructions @@ -20,7 +27,7 @@ Compile and test the project: mvn verify ---- -Generate the documentation (from the documentation folder): +Generate the documentation (from the `documentation` folder): [source,bash] ---- @@ -35,10 +42,10 @@ mkdocs serve * link:converters[] - Additional Converters * link:documentation[] - Project documentation * link:examples[] - Examples projects to demonstrate SmallRye Config features -* link:implementation[] - Implementation of the MicroProfile Config API +* link:implementation[] - Implementation of SmallRye Config * link:sources[] - Implementation of custom Config Sources * link:testsuite[] - Test suite to run the implementation against the MicroProfile Config TCK -* link:utils[] - A set of additional extensions to enhance MicroProfile Config +* link:utils[] - A set of additional extensions to enhance SmallRye Config * link:validator[] - Bean Validation integration === Contributing diff --git a/cdi/pom.xml b/cdi/pom.xml index 14f4d28c3..25d68da8d 100644 --- a/cdi/pom.xml +++ b/cdi/pom.xml @@ -21,12 +21,53 @@ io.smallrye.config smallrye-config-parent - 2.11.1 + 3.7.1 smallrye-config - SmallRye: MicroProfile Config CDI Implementation + SmallRye Config: CDI + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.2 + + + package + + shade + + + false + true + asm-shaded + + + org.objectweb.asm + io.smallrye.config.asm9 + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + META-INF/versions/** + **/module-info.class + + + + + + + + + diff --git a/cdi/src/main/java/io/smallrye/config/inject/ConfigException.java b/cdi/src/main/java/io/smallrye/config/inject/ConfigException.java index 2dfaadc89..b6344027f 100644 --- a/cdi/src/main/java/io/smallrye/config/inject/ConfigException.java +++ b/cdi/src/main/java/io/smallrye/config/inject/ConfigException.java @@ -19,7 +19,7 @@ /** * A relatively generic Exception that encapsulates the configuration * property's name. - * + * * @author Steve Moyer - The Pennsylvania State University */ public class ConfigException extends Exception { diff --git a/cdi/src/main/java/io/smallrye/config/inject/ConfigExtension.java b/cdi/src/main/java/io/smallrye/config/inject/ConfigExtension.java index 8d53e7a78..4cf745669 100644 --- a/cdi/src/main/java/io/smallrye/config/inject/ConfigExtension.java +++ b/cdi/src/main/java/io/smallrye/config/inject/ConfigExtension.java @@ -18,8 +18,6 @@ import static io.smallrye.config.ConfigMappings.registerConfigMappings; import static io.smallrye.config.ConfigMappings.registerConfigProperties; import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; -import static io.smallrye.config.inject.ConfigMappingInjectionBean.getPrefixFromInjectionPoint; -import static io.smallrye.config.inject.ConfigMappingInjectionBean.getPrefixFromType; import static io.smallrye.config.inject.ConfigProducer.isClassHandledByConfigProducer; import static io.smallrye.config.inject.InjectionMessages.formatInjectionPoint; import static io.smallrye.config.inject.SecuritySupport.getContextClassLoader; @@ -38,21 +36,22 @@ import java.util.function.Supplier; import java.util.stream.StreamSupport; -import javax.enterprise.event.Observes; -import javax.enterprise.inject.Instance; -import javax.enterprise.inject.spi.AfterBeanDiscovery; -import javax.enterprise.inject.spi.AfterDeploymentValidation; -import javax.enterprise.inject.spi.AnnotatedType; -import javax.enterprise.inject.spi.BeanManager; -import javax.enterprise.inject.spi.BeforeBeanDiscovery; -import javax.enterprise.inject.spi.Extension; -import javax.enterprise.inject.spi.InjectionPoint; -import javax.enterprise.inject.spi.ProcessAnnotatedType; -import javax.enterprise.inject.spi.ProcessInjectionPoint; -import javax.enterprise.inject.spi.WithAnnotations; -import javax.inject.Provider; - -import org.eclipse.microprofile.config.Config; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.AfterDeploymentValidation; +import jakarta.enterprise.inject.spi.AnnotatedType; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.BeforeBeanDiscovery; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.enterprise.inject.spi.ProcessAnnotatedType; +import jakarta.enterprise.inject.spi.ProcessInjectionPoint; +import jakarta.enterprise.inject.spi.WithAnnotations; +import jakarta.enterprise.inject.spi.configurator.AnnotatedTypeConfigurator; +import jakarta.enterprise.util.Nonbinding; +import jakarta.inject.Provider; + import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperties; import org.eclipse.microprofile.config.inject.ConfigProperty; @@ -69,11 +68,14 @@ */ public class ConfigExtension implements Extension { private final Set configPropertyInjectionPoints = new HashSet<>(); - - private final Set> configProperties = new HashSet<>(); - private final Set configPropertiesInjectionPoints = new HashSet<>(); - private final Set> configMappings = new HashSet<>(); - private final Set configMappingInjectionPoints = new HashSet<>(); + /** ConfigProperties for SmallRye Config */ + private final Set configProperties = new HashSet<>(); + /** ConfigProperties for CDI */ + private final Set configPropertiesBeans = new HashSet<>(); + /** ConfigMappings for SmallRye Config */ + private final Set configMappings = new HashSet<>(); + /** ConfigMappings for CDI */ + private final Set configMappingBeans = new HashSet<>(); public ConfigExtension() { } @@ -81,15 +83,33 @@ public ConfigExtension() { protected void beforeBeanDiscovery(@Observes BeforeBeanDiscovery bbd, BeanManager bm) { AnnotatedType configBean = bm.createAnnotatedType(ConfigProducer.class); bbd.addAnnotatedType(configBean, ConfigProducer.class.getName()); + + // Remove NonBinding annotation. OWB is not able to look up CDI beans programmatically with NonBinding in the + // case the look-up changed the non-binding parameters (in this case the prefix) + AnnotatedTypeConfigurator configPropertiesConfigurator = bbd + .configureQualifier(ConfigProperties.class); + configPropertiesConfigurator.methods().forEach(methodConfigurator -> methodConfigurator + .remove(annotation -> annotation.annotationType().equals(Nonbinding.class))); } protected void processConfigProperties( @Observes @WithAnnotations(ConfigProperties.class) ProcessAnnotatedType processAnnotatedType) { - // Even if we filter in the CDI event, beans containing injection points of ConfigMapping are also fired. + // Even if we filter in the CDI event, beans containing injection points of ConfigProperties are also fired. if (processAnnotatedType.getAnnotatedType().isAnnotationPresent(ConfigProperties.class)) { - // We are going to veto, because it may be a managed bean and we will use a configurator bean + // We are going to veto, because it may be a managed bean, and we will use a configurator bean processAnnotatedType.veto(); - configProperties.add(processAnnotatedType.getAnnotatedType()); + + // Each config class is both in SmallRyeConfig and managed by a configurator bean. + // CDI requires more beans for injection points due to binding prefix. + ConfigClassWithPrefix properties = configClassWithPrefix(processAnnotatedType.getAnnotatedType().getJavaClass(), + processAnnotatedType.getAnnotatedType().getAnnotation(ConfigProperties.class).prefix()); + // Unconfigured is represented as an empty String in SmallRye Config + if (!properties.getPrefix().equals(ConfigProperties.UNCONFIGURED_PREFIX)) { + configProperties.add(properties); + } else { + configProperties.add(ConfigClassWithPrefix.configClassWithPrefix(properties.getKlass(), "")); + } + configPropertiesBeans.add(properties); } } @@ -97,9 +117,15 @@ protected void processConfigMappings( @Observes @WithAnnotations(ConfigMapping.class) ProcessAnnotatedType processAnnotatedType) { // Even if we filter in the CDI event, beans containing injection points of ConfigMapping are also fired. if (processAnnotatedType.getAnnotatedType().isAnnotationPresent(ConfigMapping.class)) { - // We are going to veto, because it may be a managed bean and we will use a configurator bean + // We are going to veto, because it may be a managed bean, and we will use a configurator bean processAnnotatedType.veto(); - configMappings.add(processAnnotatedType.getAnnotatedType()); + + // Each config class is both in SmallRyeConfig and managed by a configurator bean. + // CDI requires a single configurator bean per class due to non-binding prefix. + ConfigClassWithPrefix mapping = configClassWithPrefix(processAnnotatedType.getAnnotatedType().getJavaClass(), + processAnnotatedType.getAnnotatedType().getAnnotation(ConfigMapping.class).prefix()); + configMappings.add(mapping); + configMappingBeans.add(mapping); } } @@ -109,11 +135,25 @@ protected void processConfigInjectionPoints(@Observes ProcessInjectionPoint) pip.getInjectionPoint().getType(), + pip.getInjectionPoint().getAnnotated().getAnnotation(ConfigProperties.class).prefix()); + + // If the prefix is empty at the injection point, fallbacks to the class prefix (already registered) + if (!properties.getPrefix().equals(ConfigProperties.UNCONFIGURED_PREFIX)) { + configProperties.add(properties); + } + // Cover all combinations of the configurator bean for ConfigProperties because prefix is binding + configPropertiesBeans.add(properties); } + // Add to SmallRyeConfig config classes with a different prefix by injection point if (pip.getInjectionPoint().getAnnotated().isAnnotationPresent(ConfigMapping.class)) { - configMappingInjectionPoints.add(pip.getInjectionPoint()); + ConfigClassWithPrefix mapping = configClassWithPrefix((Class) pip.getInjectionPoint().getType(), + pip.getInjectionPoint().getAnnotated().getAnnotation(ConfigMapping.class).prefix()); + // If the prefix is empty at the injection point, fallbacks to the class prefix (already registered) + if (!mapping.getPrefix().isEmpty()) { + configMappings.add(mapping); + } } } @@ -125,7 +165,7 @@ protected void registerCustomBeans(@Observes AfterBeanDiscovery abd, BeanManager ParameterizedType type = (ParameterizedType) requiredType; // TODO We should probably handle all parameterized types correctly if (type.getRawType().equals(Provider.class) || type.getRawType().equals(Instance.class)) { - // These injection points are satisfied by the built-in Instance bean + // These injection points are satisfied by the built-in Instance bean Type typeArgument = type.getActualTypeArguments()[0]; if (typeArgument instanceof Class && !isClassHandledByConfigProducer(typeArgument)) { customTypes.add((Class) typeArgument); @@ -137,18 +177,28 @@ protected void registerCustomBeans(@Observes AfterBeanDiscovery abd, BeanManager } } - customTypes.stream().map(customType -> new ConfigInjectionBean<>(bm, customType)).forEach(abd::addBean); - configProperties.stream().map(annotatedType -> new ConfigMappingInjectionBean<>(bm, annotatedType)) - .forEach(abd::addBean); - configMappings.stream().map(annotatedType -> new ConfigMappingInjectionBean<>(bm, annotatedType)).forEach(abd::addBean); + customTypes.forEach(customType -> abd.addBean(new ConfigInjectionBean<>(bm, customType))); + configPropertiesBeans.forEach(properties -> abd.addBean(new ConfigPropertiesInjectionBean<>(properties))); + configMappingBeans.forEach(mapping -> abd.addBean(new ConfigMappingInjectionBean<>(mapping, bm))); } protected void validate(@Observes AfterDeploymentValidation adv) { - Config config = ConfigProvider.getConfig(getContextClassLoader()); + SmallRyeConfig config = ConfigProvider.getConfig(getContextClassLoader()).unwrap(SmallRyeConfig.class); Set configNames = StreamSupport.stream(config.getPropertyNames().spliterator(), false).collect(toSet()); for (InjectionPoint injectionPoint : getConfigPropertyInjectionPoints()) { Type type = injectionPoint.getType(); + // validate injection config name + ConfigProperty configProperty = injectionPoint.getAnnotated().getAnnotation(ConfigProperty.class); + String name; + try { + name = ConfigProducerUtil.getConfigKey(injectionPoint, configProperty); + } catch (IllegalStateException e) { + adv.addDeploymentProblem(InjectionMessages.msg.retrieveConfigFailure(null, formatInjectionPoint(injectionPoint), + e.getLocalizedMessage(), e)); + continue; + } + // We don't validate the Optional / Provider / Supplier / ConfigValue for defaultValue. if (type instanceof Class && org.eclipse.microprofile.config.ConfigValue.class.isAssignableFrom((Class) type) || type instanceof Class && OptionalInt.class.isAssignableFrom((Class) type) @@ -161,23 +211,15 @@ protected void validate(@Observes AfterDeploymentValidation adv) { continue; } - ConfigProperty configProperty = injectionPoint.getAnnotated().getAnnotation(ConfigProperty.class); - String name; - try { - name = ConfigProducerUtil.getConfigKey(injectionPoint, configProperty); - } catch (IllegalStateException e) { - adv.addDeploymentProblem( - InjectionMessages.msg.retrieveConfigFailure(null, formatInjectionPoint(injectionPoint), - e.getLocalizedMessage(), e)); - continue; - } - // Check if the name is part of the properties first. // Since properties can be a subset, then search for the actual property for a value. - // Check if it is a map - // Finally also check if the property is indexed (might be a Collection with indexed properties). - if ((!configNames.contains(name) && ConfigProducerUtil.getRawValue(name, config) == null) - && !isMap(type) && !isIndexed(type, name, config)) { + // Check if the property is indexed (might be a Collection with indexed properties). + // Check if the properti is part of a Map + if (!configNames.contains(name) + && ConfigProducerUtil.getConfigValue(name, config).getValue() == null + && !isIndexed(type, name, config) + && !isMap(type, name, config)) { + if (configProperty.defaultValue().equals(ConfigProperty.UNCONFIGURED_VALUE)) { adv.addDeploymentProblem( InjectionMessages.msg.noConfigValue(name, formatInjectionPoint(injectionPoint))); @@ -194,14 +236,9 @@ protected void validate(@Observes AfterDeploymentValidation adv) { } } - Set configMappingsWithPrefix = mapToConfigObjectWithPrefix(configMappings, - configMappingInjectionPoints); - Set configPropertiesWithPrefix = mapToConfigObjectWithPrefix(configProperties, - configPropertiesInjectionPoints); - try { - registerConfigMappings(config.unwrap(SmallRyeConfig.class), configMappingsWithPrefix); - registerConfigProperties(config.unwrap(SmallRyeConfig.class), configPropertiesWithPrefix); + registerConfigMappings(config, configMappings); + registerConfigProperties(config, configProperties); } catch (ConfigValidationException e) { adv.addDeploymentProblem(e); } @@ -211,38 +248,16 @@ protected Set getConfigPropertyInjectionPoints() { return configPropertyInjectionPoints; } - private static Set mapToConfigObjectWithPrefix( - Set> annotatedTypes, - Set injectionPoints) { - - Set configMappingsWithPrefix = new HashSet<>(); - for (AnnotatedType annotatedType : annotatedTypes) { - configMappingsWithPrefix - .add(configClassWithPrefix(annotatedType.getJavaClass(), getPrefixFromType(annotatedType))); - } - for (InjectionPoint injectionPoint : injectionPoints) { - getPrefixFromInjectionPoint(injectionPoint).ifPresent(prefix -> configMappingsWithPrefix - .add(configClassWithPrefix((Class) injectionPoint.getType(), prefix))); - } - return configMappingsWithPrefix; - } - - private static boolean isIndexed(Type type, String name, Config config) { + private static boolean isIndexed(Type type, String name, SmallRyeConfig config) { return type instanceof ParameterizedType && (List.class.isAssignableFrom((Class) ((ParameterizedType) type).getRawType()) || Set.class.isAssignableFrom((Class) ((ParameterizedType) type).getRawType())) - && - !((SmallRyeConfig) config).getIndexedPropertiesIndexes(name).isEmpty(); + && !config.getIndexedPropertiesIndexes(name).isEmpty(); } - /** - * Indicates whether the given type is a type of Map. - * - * @param type the type to check - * @return {@code true} if the given type is a type of Map, {@code false} otherwise. - */ - private static boolean isMap(final Type type) { - return type instanceof ParameterizedType && - Map.class.isAssignableFrom((Class) ((ParameterizedType) type).getRawType()); + private static boolean isMap(final Type type, String name, SmallRyeConfig config) { + return type instanceof ParameterizedType + && Map.class.isAssignableFrom((Class) ((ParameterizedType) type).getRawType()) + && !config.getMapKeys(name).isEmpty(); } } diff --git a/cdi/src/main/java/io/smallrye/config/inject/ConfigInjectionBean.java b/cdi/src/main/java/io/smallrye/config/inject/ConfigInjectionBean.java index 22bf16238..d9d3c81fc 100644 --- a/cdi/src/main/java/io/smallrye/config/inject/ConfigInjectionBean.java +++ b/cdi/src/main/java/io/smallrye/config/inject/ConfigInjectionBean.java @@ -23,16 +23,16 @@ import java.util.Optional; import java.util.Set; -import javax.enterprise.context.Dependent; -import javax.enterprise.context.spi.CreationalContext; -import javax.enterprise.inject.Instance; -import javax.enterprise.inject.spi.Annotated; -import javax.enterprise.inject.spi.Bean; -import javax.enterprise.inject.spi.BeanManager; -import javax.enterprise.inject.spi.InjectionPoint; -import javax.enterprise.inject.spi.PassivationCapable; -import javax.enterprise.util.AnnotationLiteral; -import javax.inject.Provider; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.Annotated; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.enterprise.inject.spi.PassivationCapable; +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Provider; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -73,11 +73,6 @@ public Class getBeanClass() { return ConfigInjectionBean.class; } - @Override - public boolean isNullable() { - return false; - } - @Override @SuppressWarnings("unchecked") public T create(CreationalContext context) { @@ -101,12 +96,12 @@ public T create(CreationalContext context) { } } else { Class annotatedTypeClass = (Class) annotated.getBaseType(); - if (defaultValue.length() == 0) { + if (defaultValue.isEmpty()) { return (T) getConfig().getValue(key, annotatedTypeClass); } else { Optional optionalValue = (Optional) getConfig().getOptionalValue(key, annotatedTypeClass); return optionalValue.orElseGet( - () -> (T) ((SmallRyeConfig) getConfig()).convert(defaultValue, annotatedTypeClass)); + () -> (T) getConfig().unwrap(SmallRyeConfig.class).convert(defaultValue, annotatedTypeClass)); } } diff --git a/cdi/src/main/java/io/smallrye/config/inject/ConfigMappingInjectionBean.java b/cdi/src/main/java/io/smallrye/config/inject/ConfigMappingInjectionBean.java index c657968cb..ed88adb33 100644 --- a/cdi/src/main/java/io/smallrye/config/inject/ConfigMappingInjectionBean.java +++ b/cdi/src/main/java/io/smallrye/config/inject/ConfigMappingInjectionBean.java @@ -1,48 +1,37 @@ package io.smallrye.config.inject; import static io.smallrye.config.inject.SecuritySupport.getContextClassLoader; -import static java.util.Optional.ofNullable; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.Collections; -import java.util.HashSet; -import java.util.Optional; import java.util.Set; -import java.util.stream.Stream; -import javax.enterprise.context.Dependent; -import javax.enterprise.context.spi.CreationalContext; -import javax.enterprise.inject.Default; -import javax.enterprise.inject.spi.Annotated; -import javax.enterprise.inject.spi.AnnotatedType; -import javax.enterprise.inject.spi.Bean; -import javax.enterprise.inject.spi.BeanManager; -import javax.enterprise.inject.spi.InjectionPoint; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.InjectionPoint; import org.eclipse.microprofile.config.ConfigProvider; -import org.eclipse.microprofile.config.inject.ConfigProperties; import io.smallrye.config.ConfigMapping; +import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix; import io.smallrye.config.SmallRyeConfig; public class ConfigMappingInjectionBean implements Bean { private final BeanManager bm; - private final Class klass; - private final String prefix; - private final Set qualifiers = new HashSet<>(); + private final ConfigClassWithPrefix configClassWithPrefix; - public ConfigMappingInjectionBean(final BeanManager bm, final AnnotatedType type) { + public ConfigMappingInjectionBean(final ConfigClassWithPrefix configClassWithPrefix, final BeanManager bm) { this.bm = bm; - this.klass = type.getJavaClass(); - this.prefix = getPrefixFromType(type); - this.qualifiers.add(type.isAnnotationPresent(ConfigProperties.class) ? ConfigProperties.Literal.of(prefix) - : Default.Literal.INSTANCE); + this.configClassWithPrefix = configClassWithPrefix; } @Override - public Class getBeanClass() { - return klass; + public Class getBeanClass() { + return (Class) configClassWithPrefix.getKlass(); } @Override @@ -50,18 +39,21 @@ public Set getInjectionPoints() { return Collections.emptySet(); } - @Override - public boolean isNullable() { - return false; - } - @Override public T create(final CreationalContext creationalContext) { InjectionPoint injectionPoint = (InjectionPoint) bm.getInjectableReference(new MetadataInjectionPoint(), creationalContext); - SmallRyeConfig config = (SmallRyeConfig) ConfigProvider.getConfig(getContextClassLoader()); - return config.getConfigMapping(klass, getPrefixFromInjectionPoint(injectionPoint).orElse(prefix)); + String prefix = configClassWithPrefix.getPrefix(); + if (injectionPoint != null && injectionPoint.getAnnotated() != null) { + ConfigMapping configMapping = injectionPoint.getAnnotated().getAnnotation(ConfigMapping.class); + if (configMapping != null) { + prefix = configMapping.prefix(); + } + } + + SmallRyeConfig config = ConfigProvider.getConfig(getContextClassLoader()).unwrap(SmallRyeConfig.class); + return config.getConfigMapping(getBeanClass(), prefix); } @Override @@ -71,12 +63,12 @@ public void destroy(final T instance, final CreationalContext creationalConte @Override public Set getTypes() { - return Collections.singleton(klass); + return Collections.singleton(configClassWithPrefix.getKlass()); } @Override public Set getQualifiers() { - return qualifiers; + return Collections.singleton(Default.Literal.INSTANCE); } @Override @@ -86,7 +78,7 @@ public Class getScope() { @Override public String getName() { - return this.getClass() + "_" + klass.getName(); + return this.getClass().getSimpleName() + "_" + configClassWithPrefix.getKlass().getName(); } @Override @@ -98,44 +90,4 @@ public Set> getStereotypes() { public boolean isAlternative() { return false; } - - public static String getPrefixFromType(final Annotated annotated) { - final Optional prefixFromConfigMappingClass = ofNullable(annotated.getBaseType()).map(type -> (Class) type) - .map(c -> c.getAnnotation(ConfigMapping.class)) - .map(ConfigMapping::prefix); - - final Optional prefixFromConfigPropertiesClass = ofNullable(annotated.getBaseType()) - .map(type -> (Class) type) - .map(c -> c.getAnnotation(ConfigProperties.class)) - .map(ConfigProperties::prefix) - .filter(prefix -> !prefix.equals(ConfigProperties.UNCONFIGURED_PREFIX)); - - return Stream.of(prefixFromConfigMappingClass, prefixFromConfigPropertiesClass) - .flatMap(s -> s.map(Stream::of).orElseGet(Stream::empty)) - .findFirst() - .orElse(""); - } - - public static Optional getPrefixFromInjectionPoint(final InjectionPoint injectionPoint) { - final Optional prefixFromConfigMapping = Optional.ofNullable(injectionPoint.getAnnotated()) - .map(a -> a.getAnnotation(ConfigMapping.class)) - .map(ConfigMapping::prefix) - .filter(prefix -> !prefix.isEmpty()); - - final Optional prefixFromConfigProperties = Optional.ofNullable(injectionPoint.getAnnotated()) - .map(a -> a.getAnnotation(ConfigProperties.class)) - .map(ConfigProperties::prefix) - .filter(prefix -> !prefix.equals(ConfigProperties.UNCONFIGURED_PREFIX)); - - final Optional prefixFromQualifier = injectionPoint.getQualifiers().stream() - .filter(ConfigProperties.class::isInstance) - .map(ConfigProperties.class::cast) - .map(ConfigProperties::prefix) - .filter(prefix -> !prefix.equals(ConfigProperties.UNCONFIGURED_PREFIX)) - .findFirst(); - - return Stream.of(prefixFromConfigMapping, prefixFromConfigProperties, prefixFromQualifier) - .flatMap(s -> s.map(Stream::of).orElseGet(Stream::empty)) - .findFirst(); - } } diff --git a/cdi/src/main/java/io/smallrye/config/inject/ConfigProducer.java b/cdi/src/main/java/io/smallrye/config/inject/ConfigProducer.java index 69cfbf2a3..104c3770c 100644 --- a/cdi/src/main/java/io/smallrye/config/inject/ConfigProducer.java +++ b/cdi/src/main/java/io/smallrye/config/inject/ConfigProducer.java @@ -21,10 +21,10 @@ import java.util.*; import java.util.function.Supplier; -import javax.enterprise.context.ApplicationScoped; -import javax.enterprise.context.Dependent; -import javax.enterprise.inject.Produces; -import javax.enterprise.inject.spi.InjectionPoint; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.spi.InjectionPoint; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -111,14 +111,14 @@ protected Character produceCharacterConfigProperty(InjectionPoint ip) { @Dependent @Produces @ConfigProperty - protected Optional produceOptionalConfigValue(InjectionPoint ip) { + protected Optional produceOptionalConfigProperty(InjectionPoint ip) { return ConfigProducerUtil.getValue(ip, getConfig()); } @Dependent @Produces @ConfigProperty - protected Supplier produceSupplierConfigValue(InjectionPoint ip) { + protected Supplier produceSupplierConfigProperty(InjectionPoint ip) { return () -> ConfigProducerUtil.getValue(ip, getConfig()); } @@ -168,7 +168,7 @@ protected OptionalDouble produceOptionalDoubleConfigProperty(InjectionPoint ip) @Produces @ConfigProperty protected ConfigValue produceConfigValue(InjectionPoint ip) { - return (ConfigValue) ConfigProducerUtil.getConfigValue(ip, getConfig()); + return ConfigProducerUtil.getConfigValue(ip, getConfig()); } public static boolean isClassHandledByConfigProducer(Type requiredType) { diff --git a/cdi/src/main/java/io/smallrye/config/inject/ConfigProducerUtil.java b/cdi/src/main/java/io/smallrye/config/inject/ConfigProducerUtil.java index 53da59352..9db8f3606 100644 --- a/cdi/src/main/java/io/smallrye/config/inject/ConfigProducerUtil.java +++ b/cdi/src/main/java/io/smallrye/config/inject/ConfigProducerUtil.java @@ -11,6 +11,7 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -20,16 +21,17 @@ import java.util.function.IntFunction; import java.util.function.Supplier; -import javax.enterprise.inject.spi.AnnotatedMember; -import javax.enterprise.inject.spi.AnnotatedType; -import javax.enterprise.inject.spi.InjectionPoint; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.AnnotatedMember; +import jakarta.enterprise.inject.spi.AnnotatedType; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.inject.Provider; import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigValue; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.spi.Converter; -import io.smallrye.config.Converters; +import io.smallrye.config.ConfigValue; import io.smallrye.config.SecretKeys; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.common.AbstractConverter; @@ -55,7 +57,19 @@ private ConfigProducerUtil() { * @return the converted configuration value. */ public static T getValue(InjectionPoint injectionPoint, Config config) { - return getValue(getName(injectionPoint), injectionPoint.getType(), getDefaultValue(injectionPoint), config); + return getValue(getName(injectionPoint), getType(injectionPoint), getDefaultValue(injectionPoint), config); + } + + private static Type getType(InjectionPoint injectionPoint) { + Type type = injectionPoint.getType(); + if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + if (parameterizedType.getRawType().equals(Provider.class) + || parameterizedType.getRawType().equals(Instance.class)) { + return parameterizedType.getActualTypeArguments()[0]; + } + } + return type; } /** @@ -72,38 +86,25 @@ public static T getValue(String name, Type type, String defaultValue, Config if (name == null) { return null; } + + SmallRyeConfig smallRyeConfig = config.unwrap(SmallRyeConfig.class); + ConfigValue configValue = getConfigValue(name, smallRyeConfig); + ConfigValue configValueWithDefault = configValue.withValue(resolveDefault(configValue.getValue(), defaultValue)); + if (hasCollection(type)) { - return convertValues(name, type, getRawValue(name, config), defaultValue, config); + return convertValues(configValueWithDefault, type, smallRyeConfig); } else if (hasMap(type)) { - return convertValues(name, type, defaultValue, config); + return convertMapValues(configValueWithDefault, type, smallRyeConfig); } - return ((SmallRyeConfig) config).convertValue(name, resolveDefault(getRawValue(name, config), defaultValue), - resolveConverter(type, config)); + return smallRyeConfig.convertValue(configValueWithDefault, resolveConverter(type, smallRyeConfig)); } - /** - * Converts the direct sub properties of the given parent property as a Map. - * - * @param name the name of the parent property for which we want the direct sub properties as a Map. - * @param type the {@link Type} of the configuration value to convert. - * @param defaultValue the default value to convert in case no sub properties could be found. - * @param config the configuration from which the values are retrieved. - * @param the expected type of the configuration value to convert. - * - * @return the converted configuration value. - */ - private static T convertValues(String name, Type type, String defaultValue, Config config) { - return ((SmallRyeConfig) config).convertValue(name, null, - resolveConverter(type, config, (kC, vC) -> new StaticMapConverter<>(name, defaultValue, config, kC, vC))); - } - - private static T convertValues(String name, Type type, String rawValue, String defaultValue, Config config) { - List indexedProperties = ((SmallRyeConfig) config).getIndexedProperties(name); + private static T convertValues(ConfigValue configValue, Type type, SmallRyeConfig config) { + List indexedProperties = config.getIndexedProperties(configValue.getName()); // If converting a config property which exists (i.e. myProp[1] = aValue) or no indexed properties exist for the config property - if (rawValue != null || indexedProperties.isEmpty()) { - return ((SmallRyeConfig) config).convertValue(name, resolveDefault(rawValue, defaultValue), - resolveConverter(type, config)); + if (configValue.getRawValue() != null || indexedProperties.isEmpty()) { + return config.convertValue(configValue, resolveConverter(type, config)); } BiFunction, IntFunction>, Collection> indexedConverter = (itemConverter, @@ -111,9 +112,7 @@ private static T convertValues(String name, Type type, String rawValue, Stri Collection collection = collectionFactory.apply(indexedProperties.size()); for (String indexedProperty : indexedProperties) { // Never null by the rules of converValue - collection.add( - ((SmallRyeConfig) config).convertValue(indexedProperty, getRawValue(indexedProperty, config), - itemConverter)); + collection.add(config.convertValue(getConfigValue(indexedProperty, config), itemConverter)); } return collection; }; @@ -121,37 +120,48 @@ private static T convertValues(String name, Type type, String rawValue, Stri return resolveConverterForIndexed(type, config, indexedConverter).convert(" "); } - static ConfigValue getConfigValue(InjectionPoint injectionPoint, Config config) { + private static T convertMapValues(ConfigValue configValue, Type type, SmallRyeConfig config) { + Map mapKeys = config.getMapKeys(configValue.getName()); + if (configValue.getRawValue() != null || mapKeys.isEmpty()) { + return config.convertValue(configValue, resolveConverter(type, config)); + } + + BiFunction, Converter, Map> mapConverter = (keyConverter, valueConverter) -> { + Map map = new HashMap<>(mapKeys.size()); + for (Map.Entry entry : mapKeys.entrySet()) { + map.put(keyConverter.convert(entry.getKey()), + config.convertValue(getConfigValue(entry.getValue(), config), valueConverter)); + } + return map; + }; + + return ConfigProducerUtil. resolveConverterForMap(type, config, mapConverter).convert(" "); + } + + static ConfigValue getConfigValue(InjectionPoint injectionPoint, SmallRyeConfig config) { String name = getName(injectionPoint); if (name == null) { return null; } - ConfigValue configValue = config.getConfigValue(name); + io.smallrye.config.ConfigValue configValue = config.getConfigValue(name); if (configValue.getRawValue() == null) { - if (configValue instanceof io.smallrye.config.ConfigValue) { - configValue = ((io.smallrye.config.ConfigValue) configValue).withValue(getDefaultValue(injectionPoint)); - } + configValue = configValue.withValue(getDefaultValue(injectionPoint)); } return configValue; } - static String getRawValue(String name, Config config) { - return SecretKeys.doUnlocked(() -> config.getConfigValue(name).getValue()); + static ConfigValue getConfigValue(String name, SmallRyeConfig config) { + return SecretKeys.doUnlocked(() -> config.getConfigValue(name)); } private static String resolveDefault(String rawValue, String defaultValue) { return rawValue != null ? rawValue : defaultValue; } - private static Converter resolveConverter(final Type type, final Config config) { - return resolveConverter(type, config, Converters::newMapConverter); - } - @SuppressWarnings("unchecked") - private static Converter resolveConverter(final Type type, final Config config, - final BiFunction, Converter, Converter>> mapConverterFactory) { + private static Converter resolveConverter(final Type type, final SmallRyeConfig config) { Class rawType = rawTypeOf(type); if (type instanceof ParameterizedType) { ParameterizedType paramType = (ParameterizedType) type; @@ -161,17 +171,13 @@ private static Converter resolveConverter(final Type type, final Config c } else if (rawType == Set.class) { return (Converter) newCollectionConverter(resolveConverter(typeArgs[0], config), HashSet::new); } else if (rawType == Map.class) { - return (Converter) mapConverterFactory.apply(resolveConverter(typeArgs[0], config), - resolveConverter(typeArgs[1], config)); + return (Converter) newMapConverter(resolveConverter(typeArgs[0], config), + resolveConverter(typeArgs[1], config), HashMap::new); } else if (rawType == Optional.class) { - return (Converter) newOptionalConverter(resolveConverter(typeArgs[0], config, mapConverterFactory)); + return (Converter) newOptionalConverter(resolveConverter(typeArgs[0], config)); } else if (rawType == Supplier.class) { - return resolveConverter(typeArgs[0], config, mapConverterFactory); + return resolveConverter(typeArgs[0], config); } - } else if (rawType == Map.class) { - // No parameterized types have been provided so it assumes that a Map of String is expected - return (Converter) mapConverterFactory.apply(resolveConverter(String.class, config), - resolveConverter(String.class, config)); } // just try the raw type return config.getConverter(rawType).orElseThrow(() -> InjectionMessages.msg.noRegisteredConverter(rawType)); @@ -179,7 +185,7 @@ private static Converter resolveConverter(final Type type, final Config c /** * We need to handle indexed properties in a special way, since a Collection may be wrapped in other converters. - * The issue is that in the original code the value was retrieve by calling the first converter that will delegate + * The issue is that in the original code the value was retrieved by calling the first converter that will delegate * to all the wrapped types until it finally gets the result. For indexed properties, because it requires * additional key lookups, a special converter is added to perform the work. This is mostly a workaround, since * converters do not have the proper API, and probably should not have to handle this type of logic. @@ -189,7 +195,7 @@ private static Converter resolveConverter(final Type type, final Config c @SuppressWarnings("unchecked") private static Converter resolveConverterForIndexed( final Type type, - final Config config, + final SmallRyeConfig config, final BiFunction, IntFunction>, Collection> indexedConverter) { Class rawType = rawTypeOf(type); @@ -209,7 +215,30 @@ private static Converter resolveConverterForIndexed( } } - throw new IllegalStateException(); + throw new IllegalArgumentException(); + } + + @SuppressWarnings("unchecked") + private static Converter resolveConverterForMap( + final Type type, + final SmallRyeConfig config, + final BiFunction, Converter, Map> mapConverter) { + + Class rawType = rawTypeOf(type); + if (type instanceof ParameterizedType) { + ParameterizedType paramType = (ParameterizedType) type; + Type[] typeArgs = paramType.getActualTypeArguments(); + if (rawType == Map.class) { + return (Converter) new MapKeyConverter<>(resolveConverter(typeArgs[0], config), + resolveConverter(typeArgs[1], config), mapConverter); + } else if (rawType == Optional.class) { + return (Converter) newOptionalConverter(resolveConverterForMap(typeArgs[0], config, mapConverter)); + } else if (rawType == Supplier.class) { + return resolveConverterForMap(typeArgs[0], config, mapConverter); + } + } + + throw new IllegalArgumentException(); } @SuppressWarnings("unchecked") @@ -225,13 +254,6 @@ private static Class rawTypeOf(final Type type) { } } - /** - * Indicates whether the given type is a type of Map or is a Supplier or Optional of Map. - * - * @param type the type to check - * @return {@code true} if the given type is a type of Map or is a Supplier or Optional of Map, - * {@code false} otherwise. - */ private static boolean hasMap(final Type type) { Class rawType = rawTypeOf(type); if (rawType == Map.class) { @@ -335,76 +357,25 @@ public C convert(final String value) throws IllegalArgumentException, NullPointe } } - /** - * A {@code Converter} of a Map that gives the same Map content whatever the value to convert. It actually relies on - * its parameters to convert the sub properties of a fixed parent property as a Map. - * - * @param The type of the keys. - * @param The type of the values. - */ - static final class StaticMapConverter extends AbstractConverter> { - private static final long serialVersionUID = 402894491607011464L; - - /** - * The name of the parent property for which we want the direct sub properties as a Map. - */ - private final String name; - /** - * The default value to convert in case no sub properties could be found. - */ - private final String defaultValue; - /** - * The configuration from which the values are retrieved. - */ - private final Config config; - /** - * The converter to use for the keys. - */ - private final Converter keyConverter; - /** - * The converter to use the for values. - */ - private final Converter valueConverter; - - /** - * Construct a {@code StaticMapConverter} with the given parameters. - * - * @param name the name of the parent property for which we want the direct sub properties as a Map - * @param defaultValue the default value to convert in case no sub properties could be found - * @param config the configuration from which the values are retrieved - * @param keyConverter the converter to use for the keys - * @param valueConverter the converter to use the for values - */ - StaticMapConverter(String name, String defaultValue, Config config, Converter keyConverter, - Converter valueConverter) { - this.name = name; - this.defaultValue = defaultValue; - this.config = config; + static final class MapKeyConverter extends AbstractConverter> { + private static final long serialVersionUID = -2920578756435265533L; + + private final Converter keyConverter; + private final Converter valueConverter; + private final BiFunction, Converter, Map> mapConverter; + + public MapKeyConverter( + final Converter keyConverter, + final Converter valueConverter, + final BiFunction, Converter, Map> mapConverter) { this.keyConverter = keyConverter; this.valueConverter = valueConverter; + this.mapConverter = mapConverter; } - /** - * {@inheritDoc} - * - * Gives the sub properties as a Map if they exist, otherwise gives the default value converted with a - * {@code MapConverter}. - */ @Override - public Map convert(String value) throws IllegalArgumentException, NullPointerException { - Map result = getValues(name, config, keyConverter, valueConverter); - if (result == null && defaultValue != null) { - result = newMapConverter(keyConverter, valueConverter).convert(defaultValue); - } - return result; - } - - /** - * @return the content of the direct sub properties as the requested type of Map. - */ - private static Map getValues(String name, Config config, Converter keyConverter, - Converter valueConverter) { - return SecretKeys.doUnlocked(() -> ((SmallRyeConfig) config).getValuesAsMap(name, keyConverter, valueConverter)); + public Map convert(final String value) throws IllegalArgumentException, NullPointerException { + return mapConverter.apply(keyConverter, valueConverter); } } } diff --git a/cdi/src/main/java/io/smallrye/config/inject/ConfigPropertiesInjectionBean.java b/cdi/src/main/java/io/smallrye/config/inject/ConfigPropertiesInjectionBean.java new file mode 100644 index 000000000..d0653ee06 --- /dev/null +++ b/cdi/src/main/java/io/smallrye/config/inject/ConfigPropertiesInjectionBean.java @@ -0,0 +1,89 @@ +package io.smallrye.config.inject; + +import static io.smallrye.config.inject.SecuritySupport.getContextClassLoader; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Set; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.InjectionPoint; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.inject.ConfigProperties; + +import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix; +import io.smallrye.config.SmallRyeConfig; + +public class ConfigPropertiesInjectionBean implements Bean { + private final ConfigClassWithPrefix configClassWithPrefix; + private final Set qualifiers; + + ConfigPropertiesInjectionBean(final ConfigClassWithPrefix configClassWithPrefix) { + this.configClassWithPrefix = configClassWithPrefix; + this.qualifiers = Collections.singleton(ConfigProperties.Literal.of(configClassWithPrefix.getPrefix())); + } + + @Override + public Class getBeanClass() { + return (Class) configClassWithPrefix.getKlass(); + } + + @Override + public Set getInjectionPoints() { + return Collections.emptySet(); + } + + @Override + public T create(final CreationalContext creationalContext) { + String prefix = configClassWithPrefix.getPrefix(); + if (prefix.equals(ConfigProperties.UNCONFIGURED_PREFIX)) { + prefix = configClassWithPrefix.getKlass().getAnnotation(ConfigProperties.class).prefix(); + if (prefix.equals(ConfigProperties.UNCONFIGURED_PREFIX)) { + prefix = ""; + } + } + + SmallRyeConfig config = ConfigProvider.getConfig(getContextClassLoader()).unwrap(SmallRyeConfig.class); + return config.getConfigMapping(getBeanClass(), prefix); + } + + @Override + public void destroy(final T instance, final CreationalContext creationalContext) { + + } + + @Override + public Set getTypes() { + return Collections.singleton(configClassWithPrefix.getKlass()); + } + + @Override + public Set getQualifiers() { + return qualifiers; + } + + @Override + public Class getScope() { + return Dependent.class; + } + + @Override + public String getName() { + return this.getClass().getSimpleName() + "_" + configClassWithPrefix.getKlass().getName() + "_" + + configClassWithPrefix.getPrefix(); + } + + @Override + public Set> getStereotypes() { + return Collections.emptySet(); + } + + @Override + public boolean isAlternative() { + return false; + } +} diff --git a/cdi/src/main/java/io/smallrye/config/inject/InjectionMessages.java b/cdi/src/main/java/io/smallrye/config/inject/InjectionMessages.java index d12fe48db..085018bcc 100644 --- a/cdi/src/main/java/io/smallrye/config/inject/InjectionMessages.java +++ b/cdi/src/main/java/io/smallrye/config/inject/InjectionMessages.java @@ -10,7 +10,7 @@ import java.util.NoSuchElementException; import java.util.stream.Collectors; -import javax.enterprise.inject.spi.InjectionPoint; +import jakarta.enterprise.inject.spi.InjectionPoint; import org.jboss.logging.Messages; import org.jboss.logging.annotations.Cause; @@ -49,32 +49,32 @@ ConfigException retrieveConfigFailure(@Param @Pos(1) String configPropertyName, IllegalArgumentException noRegisteredConverter(Class type); /** - * + * * Formats InjectPoint information for Exception messages.
*
- * + * * 3 possible InjectionPoint types are considered:
*
- * + * * Fields
* Given: java.lang.String * io.smallrye.config.inject.ValidateInjectionTest$SkipPropertiesTest$SkipPropertiesBean.missingProp
* Returns: io.smallrye.config.inject.ValidateInjectionTest$SkipPropertiesTest$SkipPropertiesBean.missingProp
*
- * + * * Method parameters
* Given: private void * io.smallrye.config.inject.ValidateInjectionTest$MethodUnnamedPropertyTest$MethodUnnamedPropertyBean.methodUnnamedProperty(java.lang.String)
* Returns: * io.smallrye.config.inject.ValidateInjectionTest$MethodUnnamedPropertyTest$MethodUnnamedPropertyBean.methodUnnamedProperty(String)
*
- * + * * Constructor parameters
* Given: public * io.smallrye.config.inject.ValidateInjectionTest$ConstructorUnnamedPropertyTest$ConstructorUnnamedPropertyBean(java.lang.String)
* Returns: * io.smallrye.config.inject.ValidateInjectionTest$ConstructorUnnamedPropertyTest$ConstructorUnnamedPropertyBean(String) - * + * */ public static String formatInjectionPoint(InjectionPoint injectionPoint) { diff --git a/cdi/src/main/java/io/smallrye/config/inject/MetadataInjectionPoint.java b/cdi/src/main/java/io/smallrye/config/inject/MetadataInjectionPoint.java index 5ea1e70ab..79004f251 100644 --- a/cdi/src/main/java/io/smallrye/config/inject/MetadataInjectionPoint.java +++ b/cdi/src/main/java/io/smallrye/config/inject/MetadataInjectionPoint.java @@ -6,11 +6,11 @@ import java.util.Collections; import java.util.Set; -import javax.enterprise.inject.Default; -import javax.enterprise.inject.spi.Annotated; -import javax.enterprise.inject.spi.Bean; -import javax.enterprise.inject.spi.InjectionPoint; -import javax.enterprise.util.AnnotationLiteral; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.spi.Annotated; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.enterprise.util.AnnotationLiteral; class MetadataInjectionPoint implements InjectionPoint { @Override diff --git a/cdi/src/main/java/io/smallrye/config/inject/SecuritySupport.java b/cdi/src/main/java/io/smallrye/config/inject/SecuritySupport.java index 7d2cfede2..229133270 100644 --- a/cdi/src/main/java/io/smallrye/config/inject/SecuritySupport.java +++ b/cdi/src/main/java/io/smallrye/config/inject/SecuritySupport.java @@ -19,7 +19,7 @@ import java.security.AccessController; import java.security.PrivilegedAction; -import io.smallrye.config.ConfigLogging; +import io.smallrye.config._private.ConfigLogging; /** * @author Jeff Mesnil (c) 2018 Red Hat inc. diff --git a/cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/cdi/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension similarity index 100% rename from cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension rename to cdi/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension diff --git a/cdi/src/test/java/io/smallrye/config/inject/ConfigInjectionTest.java b/cdi/src/test/java/io/smallrye/config/inject/ConfigInjectionTest.java index 423fcae61..cfa6ade09 100644 --- a/cdi/src/test/java/io/smallrye/config/inject/ConfigInjectionTest.java +++ b/cdi/src/test/java/io/smallrye/config/inject/ConfigInjectionTest.java @@ -16,8 +16,8 @@ import java.util.Set; import java.util.function.Supplier; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -57,10 +57,10 @@ void inject() { assertEquals("OK", configBean.getReasonsSupplier().get().get(200)); assertEquals("Created", configBean.getReasonsSupplier().get().get(201)); assertFalse(configBean.getReasonsOptional().isPresent()); - assertEquals(2, configBean.getVersions().size()); + assertEquals(3, configBean.getVersions().size()); assertEquals(new Version(1, "The version 1.2.3"), configBean.getVersions().get("v1")); assertEquals(new Version(2, "The version 2.0.0"), configBean.getVersions().get("v2")); - assertEquals(2, configBean.getVersionsDefault().size()); + assertEquals(3, configBean.getVersionsDefault().size()); assertEquals(new Version(1, "The version 1;2;3"), configBean.getVersionsDefault().get("v1=1;2;3")); assertEquals(new Version(2, "The version 2;1;0"), configBean.getVersionsDefault().get("v2=2;1;0")); assertEquals(1, configBean.getVersionDefault().size()); @@ -90,10 +90,12 @@ void inject() { assertEquals("5678", configBean.getMyPropProfile()); assertThrows(SecurityException.class, () -> configBean.getConfig().getValue("secret", String.class), "Not allowed to access secret key secret"); - assertEquals("1234", configBean.getConfig().getValue("my.prop", String.class)); assertEquals("1234", configBean.getSmallRyeConfig().getRawValue("my.prop")); assertEquals(HyphenatedEnum.A_B, configBean.getHyphenatedEnum()); + assertNull(configBean.getMissingExpression().getValue()); + assertFalse(configBean.getMissingExpressionOptional().isPresent()); + assertFalse(configBean.getMissingExpressionOptionalInt().isPresent()); } @Test @@ -195,6 +197,15 @@ static class ConfigBean { @Inject @ConfigProperty(name = "hyphenated.enum") HyphenatedEnum hyphenatedEnum; + @Inject + @ConfigProperty(name = "missing.expression") + ConfigValue missingExpression; + @Inject + @ConfigProperty(name = "missing.expression") + Optional missingExpressionOptional; + @Inject + @ConfigProperty(name = "missing.expression") + Optional missingExpressionOptionalInt; Optional> getReasonsOptional() { return reasonsOptional; @@ -283,6 +294,18 @@ Optional getConvertedValueOptional() { HyphenatedEnum getHyphenatedEnum() { return hyphenatedEnum; } + + ConfigValue getMissingExpression() { + return missingExpression; + } + + Optional getMissingExpressionOptional() { + return missingExpressionOptional; + } + + Optional getMissingExpressionOptionalInt() { + return missingExpressionOptionalInt; + } } @BeforeAll @@ -290,7 +313,7 @@ static void beforeAll() { SmallRyeConfig config = new SmallRyeConfigBuilder() .withSources(config("my.prop", "1234", "expansion", "${my.prop}", "secret", "12345678", "empty", "", "mp.config.profile", "prof", "my.prop.profile", "1234", "%prof.my.prop.profile", "5678", - "bad.property.expression.prop", "${missing.prop}", "reasons.200", "OK", "reasons.201", "Created", + "missing.expression", "${missing.prop}", "reasons.200", "OK", "reasons.201", "Created", "versions.v1", "1.The version 1.2.3", "versions.v1.2", "1.The version 1.2.0", "versions.v2", "2.The version 2.0.0", "lnums.even", "2,4,6,8", "lnums.odd", "1,3,5,7,9", diff --git a/cdi/src/test/java/io/smallrye/config/inject/ConfigMappingInjectionTest.java b/cdi/src/test/java/io/smallrye/config/inject/ConfigMappingInjectionTest.java index 0d2603789..417042181 100644 --- a/cdi/src/test/java/io/smallrye/config/inject/ConfigMappingInjectionTest.java +++ b/cdi/src/test/java/io/smallrye/config/inject/ConfigMappingInjectionTest.java @@ -8,8 +8,8 @@ import java.util.Optional; -import javax.enterprise.inject.spi.CDI; -import javax.inject.Inject; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; diff --git a/cdi/src/test/java/io/smallrye/config/inject/ConfigPropertiesInjectionTest.java b/cdi/src/test/java/io/smallrye/config/inject/ConfigPropertiesInjectionTest.java index d049a169b..29722b9d2 100644 --- a/cdi/src/test/java/io/smallrye/config/inject/ConfigPropertiesInjectionTest.java +++ b/cdi/src/test/java/io/smallrye/config/inject/ConfigPropertiesInjectionTest.java @@ -10,13 +10,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; -import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; -import javax.enterprise.context.Dependent; -import javax.enterprise.inject.spi.CDI; -import javax.inject.Inject; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.UnsatisfiedResolutionException; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -31,7 +31,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import io.smallrye.config.ConfigMessages; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; @@ -113,9 +112,8 @@ void empty() { assertNull(config.getConfigValue("theHost").getValue()); assertNull(config.getConfigValue("port").getValue()); - assertThrows(NoSuchElementException.class, - () -> CDI.current().select(Server.class, ConfigProperties.Literal.of("")).get(), - () -> ConfigMessages.msg.mappingNotFound(Server.class.getName()).getMessage()); + assertThrows(UnsatisfiedResolutionException.class, + () -> CDI.current().select(Server.class, ConfigProperties.Literal.of("")).get()); } @Dependent diff --git a/cdi/src/test/java/io/smallrye/config/inject/IndexedPropertiesInjectionTest.java b/cdi/src/test/java/io/smallrye/config/inject/IndexedPropertiesInjectionTest.java index 581ec2736..1cfa9f578 100644 --- a/cdi/src/test/java/io/smallrye/config/inject/IndexedPropertiesInjectionTest.java +++ b/cdi/src/test/java/io/smallrye/config/inject/IndexedPropertiesInjectionTest.java @@ -15,8 +15,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/cdi/src/test/java/io/smallrye/config/inject/InjectedConfigSerializationTest.java b/cdi/src/test/java/io/smallrye/config/inject/InjectedConfigSerializationTest.java index d653f6786..b28b0adaf 100644 --- a/cdi/src/test/java/io/smallrye/config/inject/InjectedConfigSerializationTest.java +++ b/cdi/src/test/java/io/smallrye/config/inject/InjectedConfigSerializationTest.java @@ -10,7 +10,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -73,7 +73,7 @@ void injectableConfigIsSerializable() { /** * Asserts that an object can be serialized and deserialized - * + * * @param o the object * @return the resulting object after serializing and deserializing */ diff --git a/cdi/src/test/java/io/smallrye/config/inject/MapInjectionTest.java b/cdi/src/test/java/io/smallrye/config/inject/MapInjectionTest.java new file mode 100644 index 000000000..308f82f4e --- /dev/null +++ b/cdi/src/test/java/io/smallrye/config/inject/MapInjectionTest.java @@ -0,0 +1,151 @@ +package io.smallrye.config.inject; + +import static io.smallrye.config.inject.KeyValuesConfigSource.config; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.jboss.weld.junit5.WeldInitiator; +import org.jboss.weld.junit5.WeldJunit5Extension; +import org.jboss.weld.junit5.WeldSetup; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; + +@ExtendWith(WeldJunit5Extension.class) +public class MapInjectionTest { + @WeldSetup + WeldInitiator weld = WeldInitiator.from(ConfigExtension.class, MapBean.class) + .addBeans() + .activate(ApplicationScoped.class) + .inject(this) + .build(); + + @Inject + MapBean mapBean; + + @Test + void map() { + assertEquals(3, mapBean.getMap().size()); + assertEquals("value", mapBean.getMap().get("key")); + assertEquals("value", mapBean.getMap().get("key.nested")); + assertEquals("value", mapBean.getMap().get("key.quoted")); + assertEquals("value", mapBean.getDefaults().get("default")); + assertEquals("value", mapBean.getConverted().get(new Key("key")).getValue()); + } + + @Test + void optionals() { + assertFalse(mapBean.getOptionalEmpty().isPresent()); + assertTrue(mapBean.getOptionalDefaults().isPresent()); + assertEquals("value", mapBean.getDefaults().get("default")); + } + + @ApplicationScoped + static class MapBean { + @Inject + @ConfigProperty(name = "map", defaultValue = "default=value") + Map map; + @Inject + @ConfigProperty(name = "map.defaults", defaultValue = "default=value") + Map defaults; + @Inject + @ConfigProperty(name = "converted") + Map converted; + @Inject + @ConfigProperty(name = "optionals.empty") + Optional> optionalEmpty; + @Inject + @ConfigProperty(name = "optionals.defaults", defaultValue = "default=value") + Optional> optionalDefaults; + + Map getMap() { + return map; + } + + Map getDefaults() { + return defaults; + } + + Map getConverted() { + return converted; + } + + Optional> getOptionalEmpty() { + return optionalEmpty; + } + + Optional> getOptionalDefaults() { + return optionalDefaults; + } + } + + @BeforeAll + static void beforeAll() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("map.key", "value", "map.key.nested", "value", "map.\"key.quoted\"", "value")) + .withSources(config("converted.key", "value")) + .build(); + ConfigProviderResolver.instance().registerConfig(config, Thread.currentThread().getContextClassLoader()); + } + + @AfterAll + static void afterAll() { + ConfigProviderResolver.instance().releaseConfig(ConfigProvider.getConfig()); + } + + static class Key { + private final String key; + + public Key(final String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Key key1 = (Key) o; + return key.equals(key1.key); + } + + @Override + public int hashCode() { + return Objects.hash(key); + } + } + + static class Value { + private final String value; + + public Value(final String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} diff --git a/cdi/src/test/java/io/smallrye/config/inject/OptionalInjectionTest.java b/cdi/src/test/java/io/smallrye/config/inject/OptionalInjectionTest.java index ca4c5075d..51bdc29cd 100644 --- a/cdi/src/test/java/io/smallrye/config/inject/OptionalInjectionTest.java +++ b/cdi/src/test/java/io/smallrye/config/inject/OptionalInjectionTest.java @@ -8,7 +8,7 @@ import java.util.OptionalInt; import java.util.OptionalLong; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/cdi/src/test/java/io/smallrye/config/inject/SupplierInjectionTest.java b/cdi/src/test/java/io/smallrye/config/inject/SupplierInjectionTest.java index 08d154eb6..fe5bf5ec8 100644 --- a/cdi/src/test/java/io/smallrye/config/inject/SupplierInjectionTest.java +++ b/cdi/src/test/java/io/smallrye/config/inject/SupplierInjectionTest.java @@ -10,8 +10,8 @@ import java.util.Set; import java.util.function.Supplier; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/cdi/src/test/java/io/smallrye/config/inject/ValidateInjectionTest.java b/cdi/src/test/java/io/smallrye/config/inject/ValidateInjectionTest.java index 0aed6ccec..b026d2def 100644 --- a/cdi/src/test/java/io/smallrye/config/inject/ValidateInjectionTest.java +++ b/cdi/src/test/java/io/smallrye/config/inject/ValidateInjectionTest.java @@ -15,10 +15,10 @@ import java.util.Set; import java.util.stream.Collectors; -import javax.enterprise.context.ApplicationScoped; -import javax.enterprise.context.Dependent; -import javax.enterprise.inject.spi.InjectionPoint; -import javax.inject.Inject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperties; @@ -42,53 +42,53 @@ import org.junit.platform.testkit.engine.Event; import io.smallrye.config.ConfigMapping; -import io.smallrye.config.ConfigMessages; import io.smallrye.config.ConfigValidationException; import io.smallrye.config.ConfigValue; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config._private.ConfigMessages; /** * The Exception messages caused by Config CDI should have the format:
- * + * *
  * {@link org.jboss.weld.exceptions.DeploymentException}: SRCFG0200X: < a SmallRye {@link InjectionMessages}> (+ where appropriate) SRCFG0000X: < a SmallRye {@link ConfigMessages}>
  * ...
  * caused by:
- * {@link io.smallrye.config.inject.ConfigInjectionException}: SRCFG0200X: < a SmallRye {@link InjectionMessages}> (+ where appropriate) SRCFG0000X: < a SmallRye {@link ConfigMessages}>
+ * {@link io.smallrye.config.inject.ConfigException}: SRCFG0200X: < a SmallRye {@link InjectionMessages}> (+ where appropriate) SRCFG0000X: < a SmallRye {@link ConfigMessages}>
  * ...
  * caused by: (where appropriate)
  * the.root.cause.Exception: SRCFG0000X: < a SmallRye {@link ConfigMessages}>
  * ...
  * 
- * + * * If n Exception messages are thrown during - * {@link ConfigExtension#validate(javax.enterprise.inject.spi.AfterDeploymentValidation)}, + * {@link ConfigExtension#validate(jakarta.enterprise.inject.spi.AfterDeploymentValidation)}, * as defined by * {@link org.jboss.weld.exceptions.DeploymentException#DeploymentException(List)} * the messages will be bundled together as follows: - * + * *
  * {@link org.jboss.weld.exceptions.DeploymentException}: Exception List with n exceptions:
  * Exception 0 :
- * {@link io.smallrye.config.inject.ConfigInjectionException}: SRCFG0200X: < a SmallRye {@link InjectionMessages}> (+ where appropriate) SRCFG0000X: < a SmallRye {@link ConfigMessages}>
+ * {@link io.smallrye.config.inject.ConfigException}: SRCFG0200X: < a SmallRye {@link InjectionMessages}> (+ where appropriate) SRCFG0000X: < a SmallRye {@link ConfigMessages}>
  * ...
  * caused by: (where appropriate)
  * the.root.cause.Exception: SRCFG0000X: < a SmallRye {@link ConfigMessages}>
  * ...
  * Exception n :
- * {@link io.smallrye.config.inject.ConfigInjectionException}: SRCFG0200X: < a SmallRye {@link InjectionMessages}> (+ where appropriate) SRCFG0000X: < a SmallRye {@link ConfigMessages}>
+ * {@link io.smallrye.config.inject.ConfigException}: SRCFG0200X: < a SmallRye {@link InjectionMessages}> (+ where appropriate) SRCFG0000X: < a SmallRye {@link ConfigMessages}>
  * ...
  * caused by: (where appropriate)
  * the.root.cause.Exception: SRCFG0000X: < a SmallRye {@link ConfigMessages}>
  * 
- * + * * where each Exception is a supressedException. */ public class ValidateInjectionTest { @Test - void missingProperty() throws Exception { + void missingProperty() { DeploymentException exception = getDeploymentException(MissingPropertyTest.class); assertThat(exception).hasMessage( "SRCFG02000: Failed to Inject @ConfigProperty for key missing.property into io.smallrye.config.inject.ValidateInjectionTest$MissingPropertyTest$MissingPropertyBean.missingProp since the config property could not be found in any config source"); @@ -99,7 +99,7 @@ void missingProperty() throws Exception { } @Test - void emptyProperty() throws Exception { + void emptyProperty() { DeploymentException exception = getDeploymentException(EmptyPropertyTest.class); assertThat(exception) .hasMessageStartingWith( @@ -113,7 +113,7 @@ void emptyProperty() throws Exception { } @Test - void badProperty() throws Exception { + void badProperty() { DeploymentException exception = getDeploymentException(BadPropertyTest.class); assertThat(exception) .hasMessageStartingWith( @@ -138,7 +138,7 @@ void customConverterMissingProperty() { } @Test - void MissingConverter() { + void missingConverter() { DeploymentException exception = getDeploymentException(MissingConverterTest.class); assertThat(exception).hasMessageStartingWith( "SRCFG02001: Failed to Inject @ConfigProperty for key my.prop into io.smallrye.config.inject.ValidateInjectionTest$MissingConverterTest$MissingConverterBean.myProp SRCFG02007:"); @@ -232,15 +232,15 @@ void missingIndexedPropertiesInjection() { DeploymentException exception = getDeploymentException(MissingIndexedPropertiesInjectionTest.class); assertThat(exception).hasMessage( - "SRCFG02000: Failed to Inject @ConfigProperty for key missing.indexed[0] into io.smallrye.config.inject.ValidateInjectionTest$MissingIndexedPropertiesInjectionTest$MissingIndexedPropertiesBean.missingIndexedProp since the config property could not be found in any config source"); + "SRCFG02000: Failed to Inject @ConfigProperty for key missing.indexed into io.smallrye.config.inject.ValidateInjectionTest$MissingIndexedPropertiesInjectionTest$MissingIndexedPropertiesBean.missingIndexedProp since the config property could not be found in any config source"); assertThat(exception.getCause()).isInstanceOf(ConfigException.class); assertThat(exception.getCause()).hasMessage( - "SRCFG02000: Failed to Inject @ConfigProperty for key missing.indexed[0] into io.smallrye.config.inject.ValidateInjectionTest$MissingIndexedPropertiesInjectionTest$MissingIndexedPropertiesBean.missingIndexedProp since the config property could not be found in any config source"); + "SRCFG02000: Failed to Inject @ConfigProperty for key missing.indexed into io.smallrye.config.inject.ValidateInjectionTest$MissingIndexedPropertiesInjectionTest$MissingIndexedPropertiesBean.missingIndexedProp since the config property could not be found in any config source"); } @Test - void BadIndexedPropertiesInjection() { + void badIndexedPropertiesInjection() { DeploymentException exception = getDeploymentException(BadIndexedPropertiesInjectionTest.class); assertThat(exception) @@ -279,11 +279,11 @@ void manyInjectionExceptions() { void missingSubProperties() { DeploymentException exception = getDeploymentException(MissingSubPropertiesTest.class); assertThat(exception).hasMessageStartingWith( - "SRCFG02001: Failed to Inject @ConfigProperty for key missing.sub.properties into io.smallrye.config.inject.ValidateInjectionTest$MissingSubPropertiesTest$MissingSubPropertiesBean.missingSubProps SRCFG00014"); + "SRCFG02000: Failed to Inject @ConfigProperty for key missing.sub.properties into io.smallrye.config.inject.ValidateInjectionTest$MissingSubPropertiesTest$MissingSubPropertiesBean.missingSubProps since the config property could not be found in any config source"); assertThat(exception.getCause()).isInstanceOf(ConfigException.class); assertThat(exception.getCause()).hasMessageStartingWith( - "SRCFG02001: Failed to Inject @ConfigProperty for key missing.sub.properties into io.smallrye.config.inject.ValidateInjectionTest$MissingSubPropertiesTest$MissingSubPropertiesBean.missingSubProps SRCFG00014"); + "SRCFG02000: Failed to Inject @ConfigProperty for key missing.sub.properties into io.smallrye.config.inject.ValidateInjectionTest$MissingSubPropertiesTest$MissingSubPropertiesBean.missingSubProps since the config property could not be found in any config source"); } @Test @@ -317,7 +317,7 @@ private DeploymentException getDeploymentException(Class clazz) { Throwable exception = failingEvents.get(0) .getPayload(TestExecutionResult.class).get().getThrowable().get(); - assertThat(exception).isInstanceOf(DeploymentException.class); // the exception should be a DeploymentException + assertThat(exception).isInstanceOf(DeploymentException.class); // the exception should be a DeploymentException return (DeploymentException) exception; } @@ -344,7 +344,6 @@ static class MissingPropertyBean { @Inject @ConfigProperty(name = "missing.property") String missingProp; - } } @@ -442,7 +441,6 @@ void fail() { Assertions.fail(); } - @SuppressWarnings("serial") static class MyType implements Converter { @Override public MyType convert(String value) { @@ -617,7 +615,7 @@ void fail() { @ApplicationScoped static class MissingPropertyExpressionBean { @Inject - @ConfigProperty(name = "bad.property.expression.prop") // Exists but contains ${missing.prop} which doesn't + @ConfigProperty(name = "bad.property.expression.prop") // Exists but contains ${missing.prop} which doesn't String missingExpressionProp; } } @@ -642,8 +640,8 @@ void fail() { @ApplicationScoped static class MissingIndexedPropertiesBean { @Inject - @ConfigProperty(name = "missing.indexed[0]") // missing.indexed[0] doesn't exist - String missingIndexedProp; + @ConfigProperty(name = "missing.indexed") // missing.indexed doesn't exist + List missingIndexedProp; } } diff --git a/common/pom.xml b/common/pom.xml index ff8aacdb3..665dd71e1 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -5,12 +5,12 @@ io.smallrye.config smallrye-config-parent - 2.11.1 + 3.7.1 smallrye-config-common - SmallRye: Common classes + SmallRye Config: Common diff --git a/common/src/main/java/io/smallrye/config/common/utils/ConfigSourceUtil.java b/common/src/main/java/io/smallrye/config/common/utils/ConfigSourceUtil.java index 7cd47b53b..189051826 100644 --- a/common/src/main/java/io/smallrye/config/common/utils/ConfigSourceUtil.java +++ b/common/src/main/java/io/smallrye/config/common/utils/ConfigSourceUtil.java @@ -53,8 +53,22 @@ private ConfigSourceUtil() { public static Map propertiesToMap(Properties properties) { Map map = new HashMap<>(); synchronized (properties) { - for (Map.Entry e : properties.entrySet()) { - map.put(String.valueOf(e.getKey()), String.valueOf(e.getValue())); + for (Map.Entry entry : properties.entrySet()) { + String key; + try { + key = String.valueOf(entry.getKey()); + } catch (Exception e) { + continue; + } + + String value; + try { + value = String.valueOf(entry.getValue()); + } catch (Exception e) { + continue; + } + + map.put(key, value); } } return map; diff --git a/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java b/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java index 37487f443..6faefd8f3 100644 --- a/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java +++ b/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.smallrye.config.common.utils; import java.util.ArrayList; @@ -88,17 +87,27 @@ public static String[] split(String text) { return list.toArray(NO_STRINGS); } + public static boolean isAsciiLetterOrDigit(char c) { + return 'a' <= c && c <= 'z' || + 'A' <= c && c <= 'Z' || + '0' <= c && c <= '9'; + } + public static String replaceNonAlphanumericByUnderscores(final String name) { + return replaceNonAlphanumericByUnderscores(name, new StringBuilder(name.length())); + } + + public static String replaceNonAlphanumericByUnderscores(final String name, final StringBuilder sb) { int length = name.length(); - StringBuilder sb = new StringBuilder(length); for (int i = 0; i < length; i++) { char c = name.charAt(i); - if ('a' <= c && c <= 'z' || - 'A' <= c && c <= 'Z' || - '0' <= c && c <= '9') { + if (isAsciiLetterOrDigit(c)) { sb.append(c); } else { sb.append('_'); + if (c == '"' && i + 1 == length) { + sb.append('_'); + } } } return sb.toString(); @@ -106,63 +115,149 @@ public static String replaceNonAlphanumericByUnderscores(final String name) { public static String toLowerCaseAndDotted(final String name) { int length = name.length(); - int beginSegment = 0; + + if (length == 0) { + return name; + } + + byte[] result; + if (length > 1 && name.charAt(length - 1) == '_' && name.charAt(length - 2) == '_') { // last quoted segment + length--; + } + result = new byte[length]; + + int i = 0; + if (name.charAt(0) == '_') { + if (name.length() > 1 && isAsciiLetterOrDigit(name.charAt(1))) { // starting single _ is a profile + result[0] = '%'; + i++; + } + } + boolean quotesOpen = false; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < length; i++) { + for (; i < length; i++) { char c = name.charAt(i); if ('_' == c) { - if (i == 0) { - // leading _ can only mean a profile - sb.append("%"); - continue; - } - - // Do not convert to index if the first segment is a number - if (beginSegment > 0) { - try { - String segment = sb.substring(beginSegment, i); - Integer.parseInt(segment); - sb.replace(beginSegment - 1, beginSegment, "[").append("]"); - - int j = i + 1; - if (j < length) { - if ('_' == name.charAt(j)) { - sb.append("."); + int next = i + 1; + if (quotesOpen) { + if (next == length) { + result[i] = '"'; // ending quotes + } else if (name.charAt(next) == '_') { // double _ end quote + result[i] = '"'; + result[next] = '.'; + i++; + quotesOpen = false; + } else { + result[i] = '.'; + } + } else if (next < length) { + char d = name.charAt(next); + if (Character.isDigit(d)) { // maybe index + result[next] = (byte) d; + int j = next + 1; + for (; j < length; j++) { + d = name.charAt(j); + if (Character.isDigit(d)) { // index + result[j] = (byte) d; + } else if ('_' == d) { // ending index + result[i] = '['; + result[j] = ']'; i = j; + break; + } else { // not an index + result[i] = '.'; + break; } } - continue; - } catch (NumberFormatException e) { - // Ignore, it is not an indexed number - } - } - - int j = i + 1; - if (j < length) { - if ('_' == name.charAt(j) && !quotesOpen) { - sb.append("."); - sb.append("\""); - i = j; + } else if (name.charAt(next) == '_') { // double _ start quote + result[i] = '.'; + result[next] = '"'; + i++; quotesOpen = true; - } else if ('_' == name.charAt(j) && quotesOpen) { - sb.append("\""); - sb.append("."); - i = j; - quotesOpen = false; } else { - sb.append("."); + result[i] = '.'; } } else { - sb.append("."); + result[i] = '.'; } - beginSegment = j; } else { - sb.append(Character.toLowerCase(c)); + result[i] = (byte) Character.toLowerCase(c); } } - return sb.toString(); + + // https://bugs.openjdk.org/browse/JDK-8295496 + return new String(result, 0, 0, result.length); + } + + public static boolean isNumeric(final CharSequence digits) { + return isNumeric(digits, 0, digits.length()); + } + + public static boolean isNumeric(final CharSequence digits, final int begin, final int end) { + if (digits.length() == 0) { + return false; + } + + if (begin == end) { + return false; + } + + for (int i = begin; i < end; i++) { + if (!Character.isDigit(digits.charAt(i))) { + return false; + } + } + return true; + } + + public static String unquoted(final String name) { + return unquoted(name, 0); + } + + public static String unquoted(final String name, final int begin) { + return unquoted(name, begin, name.length()); + } + + public static String unquoted(final String name, final int begin, final int end) { + if (begin < 0 || begin > end || end > name.length()) { + throw new StringIndexOutOfBoundsException("begin " + begin + ", end " + end + ", length " + name.length()); + } + + if (name.length() < 2 || name.length() <= begin) { + return name; + } + + if (name.charAt(begin) == '"' && name.charAt(end - 1) == '"') { + return name.substring(begin + 1, end - 1); + } else { + return name.substring(begin, end); + } + } + + public static int index(final String name) { + if (name.charAt(name.length() - 1) == ']') { + int start = name.lastIndexOf('['); + if (start != -1 && isNumeric(name, start + 1, name.length() - 1)) { + return Integer.parseInt(name.substring(start + 1, name.length() - 1)); + } + } + throw new IllegalArgumentException(); + } + + public static String unindexed(final String name) { + if (name.length() < 3) { + return name; + } + + if (name.charAt(name.length() - 1) == ']') { + int begin = name.lastIndexOf('['); + if (begin != -1 && isNumeric(name, begin + 1, name.length() - 1)) { + return name.substring(0, begin); + } + } + + return name; } public static String skewer(String camelHumps) { @@ -170,77 +265,64 @@ public static String skewer(String camelHumps) { } public static String skewer(String camelHumps, char separator) { - return skewer(camelHumps, 0, camelHumps.length(), new StringBuilder(), separator); - } - - private static String skewer(String camelHumps, int start, int end, StringBuilder b, char separator) { if (camelHumps.isEmpty()) { - throw new IllegalArgumentException("Method seems to have an empty name"); - } - int cp = camelHumps.codePointAt(start); - b.appendCodePoint(Character.toLowerCase(cp)); - start += Character.charCount(cp); - if (start == end) { - // a lonely character at the end of the string - return b.toString(); - } - if (Character.isUpperCase(cp)) { - // all-uppercase words need one code point of lookahead - int nextCp = camelHumps.codePointAt(start); - if (Character.isUpperCase(nextCp)) { - // it's some kind of `WORD` - for (;;) { - b.appendCodePoint(Character.toLowerCase(nextCp)); - start += Character.charCount(cp); - cp = nextCp; - if (start == end) { - return b.toString(); - } - nextCp = camelHumps.codePointAt(start); - // combine non-letters in with this name - if (Character.isLowerCase(nextCp)) { + return camelHumps; + } + + int end = camelHumps.length(); + StringBuilder b = new StringBuilder(); + + for (int i = 0; i < end; i++) { + char c = camelHumps.charAt(i); + if (Character.isLowerCase(c)) { + b.append(c); + } else if (Character.isUpperCase(c)) { + if (i > 0) { + char last = camelHumps.charAt(i - 1); + if (last != '_' && last != '-') { b.append(separator); - return skewer(camelHumps, start, end, b, separator); } } - // unreachable - } else { - // it was the start of a `Word`; continue until we hit the end or an uppercase. - b.appendCodePoint(nextCp); - start += Character.charCount(nextCp); - for (;;) { - if (start == end) { - return b.toString(); + b.append(Character.toLowerCase(c)); + // lookahead for all upper case words, like fooBAR transform to foo_bar and not foo_b_a_r + // move caret by 1 + int j = i + 1; + for (; j < end; j++) { + char u = camelHumps.charAt(j); + if (Character.isUpperCase(u)) { + b.append(Character.toLowerCase(u)); + } else if (Character.isDigit(u) || u == '-') { + // A digit in the middle will break the all upper case word, the main cycle can resume + b.append(u); + } else { + // it is an all upper case word if j > i + 1, the initial value + if (j > i + 1 && u != '_' && !Character.isDigit(b.charAt(b.length() - 1))) { + // all upper case word done, but last upper starts a new word, so we need to insert the separator + b.insert(b.length() - 1, separator); + } + // we don't know what is coming next, so we go back and let the main cycle handle it + j--; + break; } - cp = camelHumps.codePointAt(start); - // combine non-letters in with this name - if (Character.isUpperCase(cp)) { + } + // restore caret + i = j; + } else if (Character.isDigit(c)) { + b.append(c); + } else if (c == '.' || c == '*' || c == '[' || c == ']') { + b.append(c); + } else { + if (i > 0) { + char last = camelHumps.charAt(i - 1); + if (last != '_' && last != '-') { b.append(separator); - return skewer(camelHumps, start, end, b, separator); } - b.appendCodePoint(cp); - start += Character.charCount(cp); - } - // unreachable - } - // unreachable - } else { - // it's some kind of `word` - for (;;) { - cp = camelHumps.codePointAt(start); - // combine non-letters in with this name - if (Character.isUpperCase(cp)) { - b.append(separator); - return skewer(camelHumps, start, end, b, separator); - } - b.appendCodePoint(cp); - start += Character.charCount(cp); - if (start == end) { - return b.toString(); + } else { + b.append(c); } } - // unreachable } - // unreachable + + return b.toString(); } } diff --git a/common/src/test/java/io/smallrye/config/common/utils/ConfigSourceUtilTest.java b/common/src/test/java/io/smallrye/config/common/utils/ConfigSourceUtilTest.java index 719553dbe..2b52e1dc8 100644 --- a/common/src/test/java/io/smallrye/config/common/utils/ConfigSourceUtilTest.java +++ b/common/src/test/java/io/smallrye/config/common/utils/ConfigSourceUtilTest.java @@ -16,6 +16,7 @@ package io.smallrye.config.common.utils; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Map; import java.util.Properties; @@ -35,4 +36,20 @@ void propertiesToMap() { assertEquals("my.value2", map.get("my.key2")); assertEquals("2", map.get("my.key3")); } + + @Test + void unableToConvertToString() { + Properties properties = new Properties(); + properties.put("foo.bar", new UnconvertableString()); + + Map map = ConfigSourceUtil.propertiesToMap(properties); + assertTrue(map.isEmpty()); + } + + private static class UnconvertableString { + @Override + public String toString() { + throw new UnsupportedOperationException(); + } + } } diff --git a/common/src/test/java/io/smallrye/config/common/utils/StringUtilTest.java b/common/src/test/java/io/smallrye/config/common/utils/StringUtilTest.java index 5a7d51262..6aca90dfe 100644 --- a/common/src/test/java/io/smallrye/config/common/utils/StringUtilTest.java +++ b/common/src/test/java/io/smallrye/config/common/utils/StringUtilTest.java @@ -16,7 +16,8 @@ package io.smallrye.config.common.utils; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -96,19 +97,131 @@ void escapingSituations() { @Test void skewer() { - assertThrows(IllegalArgumentException.class, () -> StringUtil.skewer("")); - assertThrows(IllegalArgumentException.class, () -> StringUtil.skewer("", '.')); + assertEquals("sigusr1", StringUtil.skewer("sigusr1")); + + assertEquals("", StringUtil.skewer("")); + assertEquals(".", StringUtil.skewer(".")); assertEquals("my-property", StringUtil.skewer("myProperty")); assertEquals("my.property", StringUtil.skewer("myProperty", '.')); + assertEquals("-", StringUtil.skewer("-")); + assertEquals("_", StringUtil.skewer("_")); assertEquals("a", StringUtil.skewer("a")); + assertEquals("a", StringUtil.skewer("A")); assertEquals("a", StringUtil.skewer("a", '.')); + assertEquals("a-b", StringUtil.skewer("a-b")); + assertEquals("_a", StringUtil.skewer("_a")); + assertEquals("_a-b", StringUtil.skewer("_a_b")); assertEquals("my-property-abc", StringUtil.skewer("myPropertyABC")); assertEquals("my.property.abc", StringUtil.skewer("myPropertyABC", '.')); - assertEquals("my-property-abc-abc", StringUtil.skewer("myPropertyABCabc")); - assertEquals("my.property.abc.abc", StringUtil.skewer("myPropertyABCabc", '.')); + assertEquals("my-property-ab-cabc", StringUtil.skewer("myPropertyABCabc")); + assertEquals("my.property.ab.cabc", StringUtil.skewer("myPropertyABCabc", '.')); + + assertEquals("is-same-rm-override", StringUtil.skewer("isSameRMOverride")); + assertEquals("http-client-http-conduit-factory", StringUtil.skewer("HttpClientHTTPConduitFactory")); + assertEquals("url-connection-http-conduit-factory", StringUtil.skewer("URLConnectionHTTPConduitFactory")); + assertEquals("abc-default", StringUtil.skewer("ABCDefault")); + assertEquals("abc", StringUtil.skewer("ABC")); + + assertEquals("discard", StringUtil.skewer("discard")); + assertEquals("a-b", StringUtil.skewer("A_B")); + assertEquals("read-uncommitted", StringUtil.skewer("READ_UNCOMMITTED")); + assertEquals("_read-uncommitted", StringUtil.skewer("_READ_UNCOMMITTED")); + assertEquals("read-uncommitted", StringUtil.skewer("READ__UNCOMMITTED")); + assertEquals("_read-uncommitted", StringUtil.skewer("_READ__UNCOMMITTED")); + assertEquals("sigusr1", StringUtil.skewer("SIGUSR1")); + assertEquals("sigusr1", StringUtil.skewer("sigusr1")); + assertEquals("trend-breaker", StringUtil.skewer("TrendBreaker")); + assertEquals("making-life-difficult", StringUtil.skewer("MAKING_LifeDifficult")); + assertEquals("making-life-difficult", StringUtil.skewer("makingLifeDifficult")); + + assertEquals("foo.bar", StringUtil.skewer("foo.bar")); + assertEquals("foo.bar-baz", StringUtil.skewer("foo.barBaz")); + assertEquals("foo.bar-baz[0]", StringUtil.skewer("foo.barBaz[0]")); + assertEquals("foo.bar-baz[*]", StringUtil.skewer("foo.barBaz[*]")); + + assertEquals("across-b2b", StringUtil.skewer("acrossB2b")); + assertEquals("across-b22b", StringUtil.skewer("acrossB22b")); + assertEquals("across-b22b", StringUtil.skewer("acrossB22B")); + assertEquals("across-b22bb", StringUtil.skewer("acrossB22BB")); + assertEquals("across-b22b-bb", StringUtil.skewer("acrossB22BBb")); + } + + @Test + void replaceNonAlphanumericByUnderscores() { + assertEquals("TEST_LANGUAGE__DE_ETR__", + StringUtil.replaceNonAlphanumericByUnderscores("test.language.\"de.etr\"".toUpperCase())); + } + + @Test + void replaceNonAlphanumericByUnderscoresWithStringBuilder() { + StringBuilder builder = new StringBuilder(); + builder.setLength(0); + assertEquals("FOO_BAR", StringUtil.replaceNonAlphanumericByUnderscores("foo.bar", builder).toUpperCase()); + builder.setLength(0); + assertEquals("FOO_BAR_BAZ", StringUtil.replaceNonAlphanumericByUnderscores("foo.bar.baz", builder).toUpperCase()); + builder.setLength(0); + assertEquals("FOO", StringUtil.replaceNonAlphanumericByUnderscores("foo", builder).toUpperCase()); + builder.setLength(0); + assertEquals("TEST_LANGUAGE__DE_ETR__", + StringUtil.replaceNonAlphanumericByUnderscores("test.language.\"de.etr\"".toUpperCase())); + } + + @Test + void toLowerCaseAndDotted() { + assertEquals("", StringUtil.toLowerCaseAndDotted("")); + assertEquals("f", StringUtil.toLowerCaseAndDotted("F")); + assertEquals("foo", StringUtil.toLowerCaseAndDotted("FOO")); + assertEquals("foo.bar", StringUtil.toLowerCaseAndDotted("FOO_BAR")); + assertEquals("foo.bar.baz", StringUtil.toLowerCaseAndDotted("FOO_BAR_BAZ")); + assertEquals("foo.bar.baz2", StringUtil.toLowerCaseAndDotted("FOO_BAR_BAZ2")); + assertEquals("foo.bar.2baz", StringUtil.toLowerCaseAndDotted("FOO_BAR_2BAZ")); + assertEquals("foo.bar.baz20", StringUtil.toLowerCaseAndDotted("FOO_BAR_BAZ20")); + assertEquals("foo.bar.20baz", StringUtil.toLowerCaseAndDotted("FOO_BAR_20BAZ")); + assertEquals("foo.bar.baz[20]", StringUtil.toLowerCaseAndDotted("FOO_BAR_BAZ_20_")); + assertEquals("foo.bar.\"baz\".i[20].e", StringUtil.toLowerCaseAndDotted("FOO_BAR__BAZ__I_20__E")); + assertEquals("test.language.\"de.etr\"", StringUtil.toLowerCaseAndDotted("TEST_LANGUAGE__DE_ETR__")); + assertEquals("%foo.bar", StringUtil.toLowerCaseAndDotted("_FOO_BAR")); + assertEquals(".\"foo.bar", StringUtil.toLowerCaseAndDotted("__FOO_BAR")); + } + + @Test + void isNumeric() { + assertTrue(StringUtil.isNumeric("0")); + assertFalse(StringUtil.isNumeric("false")); + assertTrue(StringUtil.isNumeric("foo[0]", 4, 5)); + assertTrue(StringUtil.isNumeric(new StringBuilder("foo[0]"), 4, 5)); + } + + @Test + void unquoted() { + assertEquals("", StringUtil.unquoted("")); + assertEquals("", StringUtil.unquoted("\"\"")); + assertEquals("a", StringUtil.unquoted("a")); + assertEquals("unquoted", StringUtil.unquoted("\"unquoted\"")); + assertEquals("my.\"unquoted\"", StringUtil.unquoted("my.\"unquoted\"")); + assertEquals("unquoted", StringUtil.unquoted("my.\"unquoted\"", 3, 13)); + assertEquals("unquoted", StringUtil.unquoted("my.unquoted", 3, 11)); + } + + @Test + void index() { + assertEquals(0, StringUtil.index("foo[0]")); + } + + @Test + void unIndexed() { + assertEquals("", StringUtil.unindexed("")); + assertEquals("[]", StringUtil.unindexed("[]")); + assertEquals("", StringUtil.unindexed("[0]")); + assertEquals("", StringUtil.unindexed("[999]")); + assertEquals("[abc]", StringUtil.unindexed("[abc]")); + assertEquals("my.prop", StringUtil.unindexed("my.prop[0]")); + assertEquals("my.prop[]", StringUtil.unindexed("my.prop[]")); + assertEquals("my.prop[", StringUtil.unindexed("my.prop[")); + assertEquals("my.prop]", StringUtil.unindexed("my.prop]")); } } diff --git a/converters/json/pom.xml b/converters/json/pom.xml index 9efd03040..7c6b03f0a 100644 --- a/converters/json/pom.xml +++ b/converters/json/pom.xml @@ -5,13 +5,16 @@ io.smallrye.config smallrye-config-parent - 2.11.1 - ../../ + 3.7.1 + ../../pom.xml smallrye-config-converter-json - - SmallRye: MicroProfile Config Converter - Json + SmallRye Config: Converter - Json + + + 1.1.5 + @@ -36,8 +39,9 @@ test - org.glassfish - jakarta.json + org.eclipse.parsson + parsson + ${version.parsson} test diff --git a/converters/json/src/main/java/io/smallrye/config/converter/json/JsonArrayConverter.java b/converters/json/src/main/java/io/smallrye/config/converter/json/JsonArrayConverter.java index 458e808ac..81b6e3899 100644 --- a/converters/json/src/main/java/io/smallrye/config/converter/json/JsonArrayConverter.java +++ b/converters/json/src/main/java/io/smallrye/config/converter/json/JsonArrayConverter.java @@ -2,15 +2,15 @@ import java.io.StringReader; -import javax.json.Json; -import javax.json.JsonArray; -import javax.json.JsonReader; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonReader; import org.eclipse.microprofile.config.spi.Converter; /** * Converts a json string to a JSonArray - * + * * @author Phillip Kruger */ public class JsonArrayConverter implements Converter { diff --git a/converters/json/src/main/java/io/smallrye/config/converter/json/JsonObjectConverter.java b/converters/json/src/main/java/io/smallrye/config/converter/json/JsonObjectConverter.java index 3584c4bf7..ee71b50fc 100644 --- a/converters/json/src/main/java/io/smallrye/config/converter/json/JsonObjectConverter.java +++ b/converters/json/src/main/java/io/smallrye/config/converter/json/JsonObjectConverter.java @@ -2,15 +2,15 @@ import java.io.StringReader; -import javax.json.Json; -import javax.json.JsonObject; -import javax.json.JsonReader; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; import org.eclipse.microprofile.config.spi.Converter; /** * Converts a json string to a JSonObject - * + * * @author Phillip Kruger */ public class JsonObjectConverter implements Converter { diff --git a/converters/json/src/test/java/io/smallrye/config/converter/json/JsonArrayConverterTest.java b/converters/json/src/test/java/io/smallrye/config/converter/json/JsonArrayConverterTest.java index ced868d84..a6dbf5de4 100644 --- a/converters/json/src/test/java/io/smallrye/config/converter/json/JsonArrayConverterTest.java +++ b/converters/json/src/test/java/io/smallrye/config/converter/json/JsonArrayConverterTest.java @@ -4,11 +4,11 @@ import java.io.StringReader; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; -import javax.json.Json; -import javax.json.JsonArray; -import javax.json.JsonReader; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonReader; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.weld.junit5.WeldInitiator; @@ -21,7 +21,7 @@ /** * Testing the injection of a JsonArray - * + * * @author Phillip Kruger */ @ExtendWith(WeldJunit5Extension.class) diff --git a/converters/json/src/test/java/io/smallrye/config/converter/json/JsonObjectConverterTest.java b/converters/json/src/test/java/io/smallrye/config/converter/json/JsonObjectConverterTest.java index 36b6bcaf4..d81817544 100644 --- a/converters/json/src/test/java/io/smallrye/config/converter/json/JsonObjectConverterTest.java +++ b/converters/json/src/test/java/io/smallrye/config/converter/json/JsonObjectConverterTest.java @@ -4,11 +4,11 @@ import java.io.StringReader; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; -import javax.json.Json; -import javax.json.JsonObject; -import javax.json.JsonReader; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.weld.junit5.WeldInitiator; @@ -21,7 +21,7 @@ /** * Testing the injection of a JsonObject - * + * * @author Phillip Kruger */ @ExtendWith(WeldJunit5Extension.class) diff --git a/coverage/pom.xml b/coverage/pom.xml index 003d09785..fd15b9c42 100644 --- a/coverage/pom.xml +++ b/coverage/pom.xml @@ -5,7 +5,7 @@ io.smallrye.config smallrye-config-parent - 2.11.1 + 3.7.1 smallrye-config-coverage @@ -53,6 +53,23 @@ smallrye-config-source-yaml ${project.version} + + io.smallrye.config + smallrye-config-source-keystore + ${project.version} + + + + + io.smallrye.config + smallrye-config-crypto + ${project.version} + + + io.smallrye.config + smallrye-config-jasypt + ${project.version} + diff --git a/documentation/Pipfile.lock b/documentation/Pipfile.lock index 2930c435b..6486729ca 100644 --- a/documentation/Pipfile.lock +++ b/documentation/Pipfile.lock @@ -33,73 +33,94 @@ }, "importlib-metadata": { "hashes": [ - "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670", - "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23" + "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed", + "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705" ], "markers": "python_version >= '3.7'", - "version": "==4.12.0" + "version": "==6.6.0" }, "jinja2": { "hashes": [ - "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", - "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", + "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" ], + "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==3.1.2" + "version": "==3.1.3" }, "markdown": { "hashes": [ - "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874", - "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621" + "sha256:065fd4df22da73a625f14890dd77eb8040edcbd68794bcd35943be14490608b2", + "sha256:8bf101198e004dc93e84a12a7395e31aac6a9c9942848ae1d99b9d72cf9b3520" ], - "markers": "python_version >= '3.6'", - "version": "==3.3.7" + "markers": "python_version >= '3.7'", + "version": "==3.4.3" }, "markupsafe": { "hashes": [ - "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", - "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", - "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", - "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", - "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", - "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", - "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", - "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", - "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", - "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", - "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", - "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", - "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", - "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", - "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", - "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", - "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", - "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", - "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", - "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", - "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", - "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", - "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", - "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", - "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", - "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", - "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", - "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", - "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", - "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", - "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", - "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", - "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", - "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", - "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", - "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", - "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", - "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", - "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", - "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" + "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", + "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", + "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", + "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", + "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", + "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", + "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", + "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", + "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", + "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", + "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", + "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", + "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", + "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", + "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", + "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", + "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", + "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", + "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", + "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", + "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", + "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", + "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", + "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", + "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", + "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", + "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", + "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", + "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", + "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", + "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", + "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", + "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", + "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", + "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", + "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", + "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", + "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", + "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", + "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", + "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", + "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", + "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", + "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", + "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", + "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", + "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", + "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", + "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", + "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", + "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", + "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", + "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", + "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", + "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", + "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", + "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", + "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", + "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2", + "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11" ], "markers": "python_version >= '3.7'", - "version": "==2.1.1" + "version": "==2.1.3" }, "mergedeep": { "hashes": [ @@ -143,43 +164,35 @@ }, "mkdocs-material-extensions": { "hashes": [ - "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44", - "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2" + "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93", + "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945" ], - "markers": "python_version >= '3.6'", - "version": "==1.0.3" + "markers": "python_version >= '3.7'", + "version": "==1.1.1" }, "packaging": { "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" ], - "markers": "python_version >= '3.6'", - "version": "==21.3" + "markers": "python_version >= '3.7'", + "version": "==23.1" }, "pygments": { "hashes": [ - "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb", - "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519" - ], - "markers": "python_version >= '3.6'", - "version": "==2.12.0" - }, - "pymdown-extensions": { - "hashes": [ - "sha256:3ef2d998c0d5fa7eb09291926d90d69391283561cf6306f85cd588a5eb5befa0", - "sha256:ec141c0f4983755349f0c8710416348d1a13753976c028186ed14f190c8061c4" + "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c", + "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1" ], "markers": "python_version >= '3.7'", - "version": "==9.5" + "version": "==2.15.1" }, - "pyparsing": { + "pymdown-extensions": { "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" + "sha256:9a77955e63528c2ee98073a1fb3207c1a45607bc74a34ef21acd098f46c3aa8a", + "sha256:e6cbe8ace7d8feda30bc4fd6a21a073893a9a0e90c373e92d69ce5b653051f55" ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" + "index": "pypi", + "version": "==10.0" }, "python-dateutil": { "hashes": [ @@ -191,6 +204,7 @@ }, "pyyaml": { "hashes": [ + "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", @@ -202,26 +216,32 @@ "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" ], @@ -246,9 +266,11 @@ }, "termcolor": { "hashes": [ - "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" + "sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475", + "sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a" ], - "version": "==1.1.0" + "markers": "python_version >= '3.7'", + "version": "==2.3.0" }, "verspec": { "hashes": [ @@ -259,42 +281,44 @@ }, "watchdog": { "hashes": [ - "sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412", - "sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654", - "sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306", - "sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33", - "sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd", - "sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7", - "sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892", - "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609", - "sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6", - "sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1", - "sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591", - "sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d", - "sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d", - "sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c", - "sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3", - "sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39", - "sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213", - "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330", - "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428", - "sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1", - "sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846", - "sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153", - "sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3", - "sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9", - "sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658" + "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a", + "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100", + "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8", + "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc", + "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae", + "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41", + "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0", + "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f", + "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c", + "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9", + "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3", + "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709", + "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83", + "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759", + "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9", + "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3", + "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7", + "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f", + "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346", + "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674", + "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397", + "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96", + "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d", + "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a", + "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64", + "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44", + "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33" ], - "markers": "python_version >= '3.6'", - "version": "==2.1.9" + "markers": "python_version >= '3.7'", + "version": "==3.0.0" }, "zipp": { "hashes": [ - "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", - "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" + "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", + "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556" ], "markers": "python_version >= '3.7'", - "version": "==3.8.0" + "version": "==3.15.0" } }, "develop": {} diff --git a/documentation/mkdocs.yaml b/documentation/mkdocs.yaml index 49eb791c1..bb1f8509f 100644 --- a/documentation/mkdocs.yaml +++ b/documentation/mkdocs.yaml @@ -18,15 +18,18 @@ nav: - 'Mappings': config/mappings.md - 'Map Support': config/map-support.md - 'Secret Keys': config/secret-keys.md + - 'Customizer': config/customizer.md - 'Configuration Reference': config/configuration.md - Config Sources: - 'Custom': config-sources/custom.md - 'Factories': config-sources/factories.md - 'Locations': config-sources/locations.md - 'YAML': config-sources/yaml.md + - 'JSON': config-sources/json.md - 'FileSystem': config-sources/filesystem.md - 'ZooKeeper': config-sources/zookeeper.md - 'HOCON': config-sources/hocon.md + - 'KeyStore': config-sources/keystore.md - Converters: - 'Custom': converters/custom.md - 'JSON': converters/json.md @@ -87,6 +90,7 @@ theme: markdown_extensions: - toc: + toc_depth: 3 permalink: '#' - admonition - smarty diff --git a/documentation/pom.xml b/documentation/pom.xml index 43c634da4..997faea78 100644 --- a/documentation/pom.xml +++ b/documentation/pom.xml @@ -5,7 +5,7 @@ io.smallrye.config smallrye-config-parent - 2.11.1 + 3.7.1 smallrye-config-documentation @@ -18,7 +18,6 @@ org.apache.maven.plugins maven-resources-plugin - 3.3.0 update-attributes diff --git a/documentation/src/main/docs/assets/extra.css b/documentation/src/main/docs/assets/extra.css index f646e6037..9f8196c18 100644 --- a/documentation/src/main/docs/assets/extra.css +++ b/documentation/src/main/docs/assets/extra.css @@ -1 +1,9 @@ /* Add stuff here if required */ + +.md-typeset__table { + min-width: 100%; +} + +.md-typeset table:not([class]) { + display: table; +} diff --git a/documentation/src/main/docs/config-sources/custom.md b/documentation/src/main/docs/config-sources/custom.md index ec0595d91..3d8d6852f 100644 --- a/documentation/src/main/docs/config-sources/custom.md +++ b/documentation/src/main/docs/config-sources/custom.md @@ -55,8 +55,7 @@ public class InMemoryConfigSource implements ConfigSource { And registration in: -_META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource_ -```properties +```properties title="META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource" org.acme.config.InMemoryConfigSource ``` @@ -113,8 +112,7 @@ public class URLConfigSourceFactory implements ConfigSourceFactory { And registration in: -_META-INF/services/io.smallrye.config.ConfigSourceFactory_ -```properties +```properties title="META-INF/services/io.smallrye.config.ConfigSourceFactory" org.acme.config.URLConfigSourceFactory ``` diff --git a/documentation/src/main/docs/config-sources/factories.md b/documentation/src/main/docs/config-sources/factories.md index 6afed7b75..3597bc84f 100644 --- a/documentation/src/main/docs/config-sources/factories.md +++ b/documentation/src/main/docs/config-sources/factories.md @@ -41,7 +41,7 @@ import java.util.List; import org.eclipse.microprofile.config.spi.ConfigSource; -import io.smallrye.config.ConfigMessages; +import io.smallrye.config._private.ConfigMessages; import io.smallrye.config.PropertiesConfigSource; import io.smallrye.config.ConfigSourceContext; import io.smallrye.config.ConfigSourceFactory; @@ -73,10 +73,31 @@ public class FileSystemConfigSourceFactory implements ConfigSourceFactory { ``` And registration in: -_META-INF/services/io.smallrye.config.ConfigSourceFactory_ -```properties + +```properties title="META-INF/services/io.smallrye.config.ConfigSourceFactory" org.acme.config.FileSystemConfigSourceFactory ``` The `FileSystemConfigSourceFactory` look ups the configuration value for `org.acme.config.file.locations`, and uses it to set up an additional `ConfigSource`. + +Alternatively, a `ConfigurableConfigSourceFactory` accepts a `ConfigMapping` interface to configure the `ConfigSource`: + +```java +@ConfigMapping(prefix = "org.acme.config.file") +interface FileSystemConfig { + List locations(); +} +``` + +```java +public class FileSystemConfigurableConfigSourceFactory implements ConfigurableConfigSourceFactory { + @Override + public Iterable getConfigSources(ConfigSourceContext context, FileSystemConfig config) { + + } +} +``` + +With a `ConfigurableConfigSourceFactory` it is not required to look up the configuration values with +`ConfigSourceContext`. The values are automatically mapped with the defined `@ConfigMapping`. diff --git a/documentation/src/main/docs/config-sources/json.md b/documentation/src/main/docs/config-sources/json.md new file mode 100644 index 000000000..53a0e88e5 --- /dev/null +++ b/documentation/src/main/docs/config-sources/json.md @@ -0,0 +1,4 @@ +# JSON Config Source + +The [YAML Config Source](yaml.md) also accepts the JSON format as its contents. The configuration file +`META-INF/microprofile-config.yaml`, still requires to use the `yaml` extension. diff --git a/documentation/src/main/docs/config-sources/keystore.md b/documentation/src/main/docs/config-sources/keystore.md new file mode 100644 index 000000000..62c9f207e --- /dev/null +++ b/documentation/src/main/docs/config-sources/keystore.md @@ -0,0 +1,53 @@ +# KeyStore Config Source + +This Config Source allows to use a Java `KeyStore` to load configuration values. It uses an ordinal of `100`. + +The following dependency is required in the classpath to use the KeyStore Config Source: + +```xml + + io.smallrye.config + smallrye-config-source-keystore + {{attributes['version']}} + +``` + +## Create a KeyStore + +The following command creates a simple KeyStore + +```bash +keytool -importpass -alias my.secret -keystore keystore -storepass secret -storetype PKCS12 -v +``` + +The `-alias my.secret` stores the configuration property name `my.secret` in the KeyStore. The command will +interactively ask for the value to be stored in the KeyStore. + +## Read the KeyStore + +The KeyStore Config Source supports reading multiple keystore files: + +```properties +smallrye.config.source.keystore.one.path=keystore-one +smallrye.config.source.keystore.one.password=password + +smallrye.config.source.keystore.two.path=keystore-two +smallrye.config.source.keystore.two.password=password +``` + +The names are arbitrary and can be any name. The name `one` and `two` are used to distinguish both KeyStores. + +If a stored configuration property requires a [Secret Handler](../config/secret-keys.md) to decode a value, set +the handler name with `smallrye.config.source.keystore."name".handler`. + +## Configuration + +| Configuration Property | Type | Default | +|---------------------------------------------------------------------------------------------------------------------|--- |----| +| `smallrye.config.source.keystore."name".path`
The KeyStore path. | String | | +| `smallrye.config.source.keystore."name".password`
The KeyStore password. | String | | +| `smallrye.config.source.keystore."name".type`
The KeyStore type. | String | `PKCS12` | +| `smallrye.config.source.keystore."name".handler`
An Optional secret keys handler. | String | | +| `smallrye.config.source.keystore."name".aliases."key".name`
An Optional aliases key name. | String | | +| `smallrye.config.source.keystore."name".aliases."key".password`
An Optional aliases key password. | String | | +| `smallrye.config.source.keystore."name".aliases."key".handler`
An Optional aliases key secret keys handler. | String | | diff --git a/documentation/src/main/docs/config-sources/locations.md b/documentation/src/main/docs/config-sources/locations.md index 25fbcc06e..684ee7323 100644 --- a/documentation/src/main/docs/config-sources/locations.md +++ b/documentation/src/main/docs/config-sources/locations.md @@ -40,7 +40,7 @@ For relative paths, the JVM `user.dir` property defines the current directory. _A specific file_ -```properties +```properties title="./src/main/resources/additional.properties" smallrye.config.locations=./src/main/resources/additional.properties ``` @@ -48,7 +48,7 @@ If a profile `dev` is active, and an `additional-dev.properties` file exists, th _All `additional.properties` files from the classpath_ -```properties +```properties title="additional.properties" smallrye.config.locations=additional.properties ``` @@ -57,14 +57,14 @@ resource, this will also be loaded. _The `resources.properties` file from a specific jar_ -```properties +```properties title="jar:file:///user/local/app/lib/resources-.jar!/resources.properties" smallrye.config.locations=jar:file:///user/local/app/lib/resources-.jar!/resources.properties ``` -If a profile `test` is active, and an `additional-test.properties` respource exists, this will also be loaded. +If a profile `test` is active, and an `additional-test.properties` resource exists, this will also be loaded. _The `config.properties` file from a web server_ -```properties -smallrye.config.locations=http://localhost:8080/config/config. +```properties title="http://localhost:8080/config/config.properties" +smallrye.config.locations=http://localhost:8080/config/config.properties ``` diff --git a/documentation/src/main/docs/config/configuration.md b/documentation/src/main/docs/config/configuration.md index 2359424fd..29222112f 100644 --- a/documentation/src/main/docs/config/configuration.md +++ b/documentation/src/main/docs/config/configuration.md @@ -1,33 +1,9 @@ # Configuration Reference -## `smallrye.config.profile` - -The main [Profile](profiles.md) to activate. - -> * type: String[] -> * default: - -## `smallrye.config.profile.parent` - -The parent [Profile](profiles.md#parent-profile) to activate. - -> * type: String[] -> * default: - -## `smallrye.config.locations` - -[Additional config locations](../config-sources/locations.md) to be loaded with the Config. The configuration supports -multiple locations separated by a comma and each must represent a valid `java.net.URI`. - -> * type: URI[] -> * default: - -## `smallrye.config.mapping.validate-unknown` - -[Validates](mappings.md#retrieval) that a `@ConfigMapping` maps every available configuration name contained in the -mapping prefix. - -> * type: boolean -> * default: false - - +| Configuration Property | Type | Default | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|-----------| +| `smallrye.config.profile`
The main [Profile](profiles.md) to activate. | String[] | | +| `smallrye.config.profile.parent`
The parent [Profile](profiles.md#parent-profile) to activate. | String | | +| `smallrye.config.locations`
[Additional config locations](../config-sources/locations.md) to be loaded with the Config. The configuration supports multiple locations separated by a comma and each must represent a valid `java.net.URI`. | URI[] | | +| `smallrye.config.mapping.validate-unknown`
[Validates](mappings.md#retrieval) that a `@ConfigMapping` maps every available configuration name contained in the mapping prefix. | boolean | false | +| `smallrye.config.log.values`
Enable logging of configuration values lookup in DEBUG log level. | boolean | false | diff --git a/documentation/src/main/docs/config/customizer.md b/documentation/src/main/docs/config/customizer.md new file mode 100644 index 000000000..55fa78b36 --- /dev/null +++ b/documentation/src/main/docs/config/customizer.md @@ -0,0 +1,43 @@ +# Customizer + +A `SmallRyeConfigBuilderCustomizer` allows to customize a `SmallRyeConfigBuilder`, used to create the `SmallRyeConfig` +instance. + +Registration of a `SmallRyeConfigBuilderCustomizer` is done via the `ServiceLoader` mechanism by providing the +implementation classes in a `META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer` file. Alternatively, +customizers may be registered via the Programmatic API in `SmallRyeConfigBuilder#withCustomizers`. + +The `SmallRyeConfigBuilderCustomizer` may also +assign a priority by overriding the default method `int priority()`. Customizers are sorted by ascending priority and +executed in that order, meaning that higher numeric priorities will execute last, possible overriding values set by +previous customizers. + +```java title="CustomConfigBuilder" +package org.acme.config; + +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilderCustomizer; + +public class CustomConfigBuilder implements SmallRyeConfigBuilderCustomizer { + @Override + public void configBuilder(final SmallRyeConfigBuilder builder) { + builder.withDefaultValue("my.default", "1234"); + } +} +``` + +And registration in: + +```properties title="META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer" +org.acme.config.CustomConfigBuilder +``` + +The `CustomConfigBuilder` will be executed when creating a new `SmallRyeConfig` from a `SmallRyeConfigBuilder`: + +```java +SmallRyeConfig config = new SmallRyeConfigBuilder().build(); +config.getValue("my.default", int.class); +``` + +A look up to `my.default` returns the value `1234`, registered by the `CustomConfigBuilder`. + diff --git a/documentation/src/main/docs/config/environment-variables.md b/documentation/src/main/docs/config/environment-variables.md index 529fc4fe5..8fc590f3b 100644 --- a/documentation/src/main/docs/config/environment-variables.md +++ b/documentation/src/main/docs/config/environment-variables.md @@ -35,20 +35,21 @@ contains a dash or some other special character, that property name can be speci Source, with the expected dotted format. It will provide additional information to SmallRye Config to perform a two-way conversion and match the property names. -For instance: +Consider: -**Config A** -```console +```console title=".env" FOO_BAR_BAZ=VALUE ``` Will map to `foo.bar.baz` and value `value`. -**Config B** -```console +If `foo.bar-baz` is available in any source: + +```console title=".env" FOO_BAR_BAZ=VALUE ``` -```properties + +```properties title="application.properties" foo.bar-baz=default ``` diff --git a/documentation/src/main/docs/config/getting-started.md b/documentation/src/main/docs/config/getting-started.md index 31c85f0b0..bf01f6792 100644 --- a/documentation/src/main/docs/config/getting-started.md +++ b/documentation/src/main/docs/config/getting-started.md @@ -32,7 +32,7 @@ rules detailed by [Environment Variables](environment-variables.md). The MicroProfile Config configuration file `META-INF/microprofile-config.properties` in the classpath. It follows the standard convention for `properties` files. -```properties +```properties title="META-INF/microprofile-config.properties" greeting.message=hello goodbye.message=bye ``` @@ -99,7 +99,7 @@ SmallRyeConfig config; ``` - If a value if not provided for this `greeting.message`, the application startup fails with a -`javax.enterprise.inject.spi.DeploymentException: No config value of type [class java.lang.String] exists for: greeting.message`. +`jakarta.enterprise.inject.spi.DeploymentException: No config value of type [class java.lang.String] exists for: greeting.message`. - The default value `!` is injected if the configuration does not provide a value for `greeting.suffix`. - The property `greeting.name` is optional - an empty Optional is injected if the configuration does not provide a value for it. @@ -157,6 +157,7 @@ interpreted as `false` * `java.util.UUID` * `java.util.Currency` * `java.util.regex.Pattern` +* `java.nio.file.Path` * Any class with declared static methods `of`, `valueOf` or `parse` that take a `String` or a `CharSequence` * Any class with declared constructors that takes a `String` or a `CharSequence` diff --git a/documentation/src/main/docs/config/map-support.md b/documentation/src/main/docs/config/map-support.md index 5f98f15bf..73b948209 100644 --- a/documentation/src/main/docs/config/map-support.md +++ b/documentation/src/main/docs/config/map-support.md @@ -1,10 +1,11 @@ # Map Support SmallRye Config allows injecting multiple configuration parameters as a `Map`. The configuration value syntax is -represented by `=;=` Consider: +represented by `property.name.map-key=value` Consider: ```properties -server.reasons=200=OK;201=Created +server.reasons.200=OK +server.reasons.201=Created ``` The previous configuration could be injected directly in a CDI Bean: @@ -15,7 +16,7 @@ With `@ConfigProperty` @ApplicationScoped public class ConfigBean { @Inject - @ConfigProperty(name = "server.reasons", defaultValue = "200=OK;201=Created") + @ConfigProperty(name = "server.reasons") Map reasons; } ``` @@ -29,6 +30,8 @@ public class Config { } ``` +The `Map` will contains the keys `200` and `201`, which map to the values `OK` and `Created`. + !!!note Only the direct sub properties will be converted into a `Map` and diff --git a/documentation/src/main/docs/config/mappings.md b/documentation/src/main/docs/config/mappings.md index c3b29c183..1206ef4ba 100644 --- a/documentation/src/main/docs/config/mappings.md +++ b/documentation/src/main/docs/config/mappings.md @@ -68,12 +68,31 @@ Server server = config.getConfigMapping(Server.class); !!! info - Config Mapping instances are cached. They are populated when the `Config` instance is initialized. + Config Mapping instances are cached. They are populated when the `Config` instance is initialized and their values + are not updated on Config Source changes. For a Config Mapping to be valid, it needs to match every configuration property name contained in the `Config` under the specified prefix set in `@ConfigMapping`. This prevents unknown configuration properties in the `Config`. This behaviour can be disabled with the configuration `smallrye.config.mapping.validate-unknown=false`. +## Defaults + +The `io.smallrye.config.WithDefault` annotation allows to set a default property value into a mapping (and prevent +errors if the configuration value is not available in any `ConfigSource`). + +```java +public interface Defaults { + @WithDefault("foo") + String foo(); + + @WithDefault("bar") + String bar(); +} +``` + +No configuration properties are required. The `Defaults#foo()` will return the value `foo` and `Defaults#bar()` will +return the value `bar`. + ## Nested Groups A nested mapping provides a way to map sub-groups of configuration properties. @@ -132,7 +151,7 @@ server.port=8080 ### `@WithParentName` -The `io.smallrye.config.WithParent` annotation allows configurations mappings to inherit its parent container name, +The `io.smallrye.config.WithParentName` annotation allows configurations mappings to inherit its parent container name, simplifying the configuration property name required to match the mapping. ```java @@ -368,38 +387,92 @@ public interface Server { ```properties server.host=localhost server.port=8080 -server.form.login-page=login.html -server.form.error-page=error.html -server.form.landing-page=index.html +server.form.index=index.html +server.form.login.page=login.html +server.form.error.page=error.html server.aliases.localhost[0].name=prod server.aliases.localhost[1].name=127.0.0.1 +server.aliases.\"io.smallrye\"[0].name=smallrye ``` The configuration property name needs to specify an additional segment to act as the map key. The `server.form` matches -the `Server#form` `Map` and the segments `login-page`, `error-page` and `landing-page` represent the `Map` +the `Server#form` `Map` and the segments `index`, `login.page` and `error.page` represent the `Map` keys. For collection types, the key requires the indexed format. The configuration name `server.aliases.localhost[0].name` maps to the `Map> aliases()` member, where `localhost` is the `Map` key, `[0]` is the index of the -`List` where the `Alias` element will be stored, containing the name `prod`. +`List` collection where the `Alias` element will be stored, containing the name `prod`. -## Defaults +!!! info -The `io.smallrye.config.WithDefault` annotation allows to set a default property value into a mapping (and prevent -errors if the configuration value is not available in any `ConfigSource`). + They `Map` key part in the configuration property name may require quotes to delimit the key. + +### `@WithUnnamedKey` + +The `io.smallrye.config.WithUnnamedKey` annotation allows to omit a single map key in the configuration path: ```java -public interface Defaults { - @WithDefault("foo") - String foo(); +@ConfigMapping(prefix = "server") +public interface Server { + @WithUnnamedKey("localhost") + Map aliases(); + + interface Alias { + String name(); + } +} +``` - @WithDefault("bar") - String bar(); +```properties +server.aliases.name=localhost +server.aliases.prod.name=prod +``` + +The `sever.aliases.name` is an unnamed `Map` property, because it does not contain the `Map` key to populate the `Map` +entry. Due to `@WithUnnamedKey("localhost")` the `Map` key is not required in the configuration path. The key used to +look up the Map entry is given by `io.smallrye.config.WithUnnamedKey#value`: + +```java +Server server = config.getConfigMapping(Server.class); +Map localhost = server.aliases.get("localhost"); +``` + +!!! warning + + If the unnamed key (in this case `localhost`) is explicitly set in a property name, the mapping will throw an error. + +### `@WithDefaults` + +The `io.smallrye.config.WithDefaults` is a marker annotation to use only in a `Map` to return the default value for +the value element on any key lookup: + +```java +@ConfigMapping(prefix = "server") +public interface Server { + @WithDefaults + Map aliases(); + + interface Alias { + @WithDefault("localhost") + String name(); + } } ``` -No configuration properties are required. The `Defaults#foo()` will return the value `foo` and `Defaults#bar()` will -return the value `bar`. +```properties +server.aliases.prod.name=prod +``` + +A look up to the `aliases` `Map` with the key `localhost`, `any` or any other key, returns a `Alias` instance, where +`Alias.name` is `localhost`, because that is the default value. A look up to `prod` returns a `Alias` instance, where +`Alias.name` is `prod` because the property is defined in the configuration as `server.aliases.prod.name=prod`. + +```java +Server server = config.getConfigMapping(Server.class); +Map localhost = server.aliases.get("localhost"); +Map any = server.aliases.get("any"); +Map any = server.aliases.get("prod"); +``` ## ToString @@ -408,7 +481,7 @@ implementation of the `toString` method. !!! caution - Do not include a `toString` declaration in a config mapping with sensitive information + Do not include a `toString` declaration in a config mapping with sensitive information. ## Validation diff --git a/documentation/src/main/docs/config/profiles.md b/documentation/src/main/docs/config/profiles.md index 5194b2e05..aaf5eb0d1 100644 --- a/documentation/src/main/docs/config/profiles.md +++ b/documentation/src/main/docs/config/profiles.md @@ -9,12 +9,10 @@ in the same file or separate files and select between them via a profile name. To be able to set properties with the same name, each property needs to be prefixed with a percentage sign `%` followed by the profile name and a dot `.` in the syntax `%{profile-name}.config.name`: -!!! example - - ```properties - http.port=8080 - %dev.http.port=8181 - ``` +```properties title="META-INF/microprofile-config.properties" +http.port=8080 +%dev.http.port=8181 +``` To activate the profile `dev`, the configuration `smallrye.config.profile=dev` has to be set into any valid `ConfigSource`. @@ -28,16 +26,13 @@ active, yields the value `8181`. Properties for a specific profile may reside in a `microprofile-config-{profile}.properties` named file. The previous example can be expressed as: -!!! example - - - ```properties title="microprofile-config.properties" - http.port=8080 - ``` +```properties title="META-INF/microprofile-config.properties" +http.port=8080 +``` - ```properties title="microprofile-config-dev.properties" - http.port=8181 - ``` +```properties title="META-INF/microprofile-config-dev.properties" +http.port=8181 +``` In this style, the property names in the profile aware file do not need to be prefixed with the profile name. @@ -55,17 +50,15 @@ In this style, the property names in the profile aware file do not need to be pr Profile lookups are only valid if the `ConfigSource` has a higher ordinal than a lookup to the regular configuration name. Consider: -!!! example +```properties title="main.properties" +config_ordinal=1000 +http.port=8080 +``` - ```properties title="main.properties" - config_ordinal=1000 - http.port=8080 - ``` - - ```properties title="profile.properties" - config_ordinal=100 - %dev.http.port=8181 - ``` +```properties title="profile.properties" +config_ordinal=100 +%dev.http.port=8181 +``` Even with the profile `dev` active, the lookup value for `my.prop` is `1234`. This prevents lower ordinal sources to set a profile property value that cannot be overridden unless the profile property is also overridden. @@ -78,19 +71,17 @@ list of profile names: `smallrye.config.profile=common,dev`. Both `common` and ` When multiple profiles are active, the rules for profile configuration are the same. If two profiles define the same configuration, then the last listed profile has priority. Consider: -!!! example - - ```properties - smallrye.config.profile=common,dev - - my.prop=1234 - %common.my.prop=0 - %dev.my.prop=5678 - - %common.commom.prop=common - %dev.dev.prop=dev - %test.test.prop=test - ``` +```properties +smallrye.config.profile=common,dev + +my.prop=1234 +%common.my.prop=1234 +%dev.my.prop=5678 + +%common.commom.prop=common +%dev.dev.prop=dev +%test.test.prop=test +``` Then @@ -99,6 +90,26 @@ Then - `my.prop` value is `5678` - `test.prop` does not have a value +It is also possible to define multiple profile properties, with a comma-separated list of profile names: + + +```properties +%prod,dev.my.prop=1234 +``` + +The property name `common.prop` is active in both `dev` and `prod` profile. If the same property name exists in +multiple profile properties then, the property name with the most specific profile wins: + +```properties +smallrye.config.profile=dev + +%prod,dev.my.prop=1234 + +%dev.my.prop=5678 +``` + +Then `my.prop` value is `5678`. + ## Parent Profile A Parent Profile adds multiple levels of hierarchy to the current profile. The configuration @@ -107,20 +118,18 @@ A Parent Profile adds multiple levels of hierarchy to the current profile. The c When the Parent Profile is active, if a property cannot be found in the current active Profile, the config lookup fallbacks to the Parent Profile. Consider: -!!! example - - ```properties - smallrye.config.profile=dev - smallrye.config.profile.parent=common - - my.prop=1234 - %common.my.prop=0 - %dev.my.prop=5678 - - %common.commom.prop=common - %dev.dev.prop=dev - %test.test.prop=test - ``` +```properties +smallrye.config.profile=dev +smallrye.config.profile.parent=common + +my.prop=1234 +%common.my.prop=0 +%dev.my.prop=5678 + +%common.commom.prop=common +%dev.dev.prop=dev +%test.test.prop=test +``` Then @@ -138,15 +147,13 @@ Then The Parent Profile also supports multiple levels of hierarchies: -!!! example - - ```properties - smallrye.config.profile=child - %child.smallrye.config.profile.parent=parent - %parent.smallrye.config.profile.parent=grandparent - %grandparent.smallrye.config.profile.parent=greatgrandparent - %greatgrandparent.smallrye.config.profile.parent=end - ``` +```properties +smallrye.config.profile=child +%child.smallrye.config.profile.parent=parent +%parent.smallrye.config.profile.parent=grandparent +%grandparent.smallrye.config.profile.parent=greatgrandparent +%greatgrandparent.smallrye.config.profile.parent=end +``` Will load the following profiles in order: `child`, `parent`, `grandparent`, `greatgrandparent`, `end` diff --git a/documentation/src/main/docs/config/secret-handlers/encryptor.java b/documentation/src/main/docs/config/secret-handlers/encryptor.java new file mode 100644 index 000000000..1def1aee6 --- /dev/null +++ b/documentation/src/main/docs/config/secret-handlers/encryptor.java @@ -0,0 +1,90 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS info.picocli:picocli:4.5.0 +//DEPS org.jasypt:jasypt:1.9.3 + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.concurrent.Callable; + +@Command(name = "encryptor", mixinStandardHelpOptions = true) +class encryptor implements Callable { + @Option(names = {"-s", "--secret" }, description = "Secret", required = true) + String secret; + @Option(names = {"-k", "--key" }, description = "Encryption Key") + String encryptionKey; + @Option(names = { "-f", "--format" }, description = "Encryption Key Format (base64 / plain)", defaultValue = "base64") + KeyFormat encryptionKeyFormat; + @Option(names = {"-a", "--algorithm" }, description = "Algorithm", defaultValue = "AES", hidden = true) + String algorithm; + @Option(names = {"-m", "--mode" }, description = "Mode", defaultValue = "GCM", hidden = true) + String mode; + @Option(names = {"-p", "--padding" }, description = "Padding", defaultValue = "NoPadding", hidden = true) + String padding; + + public static void main(String... args) { + int exitCode = new CommandLine(new encryptor()).execute(args); + System.exit(exitCode); + } + + @Override + public Integer call() throws Exception { + if (encryptionKey == null) { + encryptionKey = encodeToString(generateEncryptionKey().getEncoded()); + } else { + if (encryptionKeyFormat.equals(KeyFormat.base64)) { + encryptionKey = encodeToString(encryptionKey.getBytes()); + } + } + + Cipher cipher = Cipher.getInstance(algorithm + "/" + mode + "/" + padding); + byte[] iv = new byte[12]; + new SecureRandom().nextBytes(iv); + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + sha256.update(encryptionKey.getBytes(StandardCharsets.UTF_8)); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sha256.digest(), "AES"), new GCMParameterSpec(128, iv)); + + byte[] encrypted = cipher.doFinal(secret.getBytes(StandardCharsets.UTF_8)); + + ByteBuffer message = ByteBuffer.allocate(1 + iv.length + encrypted.length); + message.put((byte) iv.length); + message.put(iv); + message.put(encrypted); + + String encrypt = Base64.getUrlEncoder().withoutPadding().encodeToString((message.array())); + System.out.println("${aes-gcm-nopadding::" + encrypt + "}"); + System.out.println("smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key=" + encryptionKey); + + return 0; + } + + private SecretKey generateEncryptionKey() { + try { + return KeyGenerator.getInstance(algorithm).generateKey(); + } catch (Exception e) { + System.err.println("Error while generating the encryption key: " + e); + System.exit(-1); + } + return null; + } + + private static String encodeToString(byte[] data) { + return Base64.getUrlEncoder().withoutPadding().encodeToString(data); + } + + public enum KeyFormat { + base64, + plain + } +} diff --git a/documentation/src/main/docs/config/secret-handlers/jasypt.java b/documentation/src/main/docs/config/secret-handlers/jasypt.java new file mode 100644 index 000000000..423f699b7 --- /dev/null +++ b/documentation/src/main/docs/config/secret-handlers/jasypt.java @@ -0,0 +1,43 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS info.picocli:picocli:4.5.0 +//DEPS org.jasypt:jasypt:1.9.3 + +import org.jasypt.encryption.pbe.StandardPBEStringEncryptor; +import org.jasypt.iv.RandomIvGenerator; +import org.jasypt.properties.PropertyValueEncryptionUtils; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +import java.util.concurrent.Callable; +import java.util.logging.Logger; + +@Command(name = "jasypt", mixinStandardHelpOptions = true) +class jasypt implements Callable { + @Option(names = {"-s", "--secret" }, description = "Secret", required = true) + private String secret; + @Option(names = {"-p", "--password" }, description = "Password", required = true) + private String password; + @Option(names = {"-a", "--algorithm" }, description = "Algorithm", defaultValue = "PBEWithHMACSHA512AndAES_256") + private String algorithm; + + public static void main(String... args) { + int exitCode = new CommandLine(new jasypt()).execute(args); + System.exit(exitCode); + } + + @Override + public Integer call() { + StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor(); + encryptor.setPassword(password); + encryptor.setAlgorithm(algorithm); + encryptor.setIvGenerator(new RandomIvGenerator()); + encryptor.initialize(); + + String encrypt = PropertyValueEncryptionUtils.encrypt(secret, encryptor); + System.out.println("${jasypt::" + encrypt + "}"); + + return 0; + } +} diff --git a/documentation/src/main/docs/config/secret-keys.md b/documentation/src/main/docs/config/secret-keys.md index 706910aec..376713da2 100644 --- a/documentation/src/main/docs/config/secret-keys.md +++ b/documentation/src/main/docs/config/secret-keys.md @@ -1,27 +1,149 @@ # Secret Keys +## Secret Keys Expressions + +In SmallRye Config, a secret configuration may be expressed as `${handler::value}`, where the `handler` is the name of +a `io.smallrye.config.SecretKeysHandler` to decode or decrypt the `value` separated by a double colon `::`. + +It is possible to create a custom `SecretKeysHandler` and provide different ways to decode or decrypt configuration +values. + +A custom `SecretKeysHandler` requires an implementation of `io.smallrye.config.SecretKeysHandler` or +`io.smallrye.config.SecretKeysHandlerFactory`. Each implementation requires registration via the `ServiceLoader` +mechanism, either in `META-INF/services/io.smallrye.config.SecretKeysHandler` or +`META-INF/services/io.smallrye.config.SecretKeysHandlerFactory` files. + +!!!danger + + It is not possible to mix Secret Keys Expressions with Property Expressions. + +### Crypto + +The `smallrye-config-crypto` artifact contains a few out-of-the-box `SecretKeysHandler`s ready for use. It requires +the following dependency: + +```xml + + io.smallrye.config + smallrye-config-crypto + {{attributes['version']}} + +``` + +#### AES/GCM/NoPadding `${aes-gcm-nopadding::...}` + +- The encoding length is 128. +- The secret and the encryption key (without padding) must be base 64 encoded. + +!!! example + + ```properties title="application.properties" + smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key=DDne5obnfH1RSeTg71xSZg + + my.secret=${aes-gcm-nopadding::DLTb_9zxThxeT5iAQqswEl5Dn1ju4FdM9hIyVip35t5V} + ``` + + The `${aes-gcm-nopadding::...}` `SecretKeyHandler` requires + `smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key` configuration to state the encryption key to be + used by the `aes-gcm-nopaddin` handler. + + A lookup to `my.secret` will use the `SecretKeysHandler` name `aes-gcm-nopadding` to decode the value + `DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa_kTliQBpuzTobDZxlg`. + +!!! info + + It is possible to generate the encrypted secret with the following [JBang](http://jbang.dev/) script: + + ```shell + jbang https://raw.githubusercontent.com/smallrye/smallrye-config/main/documentation/src/main/docs/config/secret-handlers/encryptor.java -s= -k=` + ``` + +##### Configuration + +| Configuration Property | Type | Default | +|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|-----------| +| `smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key`
The encryption key to use to decode secrets encoded by the `AES/GCM/NoPadding` algorithm. | String | | +| `"smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode"`
Decode the encryption key in Base64, if the plain text key was used to encrypt the secret. | boolean | false | + +### Jasypt + +[Jasypt](http://www.jasypt.org) is a java library which allows the developer to add basic encryption capabilities. Add +the following dependency in your project to use it: + +```xml + + io.smallrye.config + smallrye-config-jasypt + {{attributes['version']}} + +``` + +#### Jasypt `${jasypt::...}` + +!!! example + + ```properties title="application.properties" + smallrye.config.secret-handler.jasypt.password=jasypt + smallrye.config.secret-handler.jasypt.algorithm=PBEWithHMACSHA512AndAES_256 + + my.secret=${jasypt::ENC(wqp8zDeiCQ5JaFvwDtoAcr2WMLdlD0rjwvo8Rh0thG5qyTQVGxwJjBIiW26y0dtU)} + ``` + The `${jasypt::...}` `SecretKeyHandler` requires both `smallrye.config.secret-handler.jasypt.password` and + `smallrye.config.secret-handler.jasypt.algorithm` configurations to state the password and the algorithm to be + used by the Jasypt encryptor. + + Jasypt encrypted values must be set with the handler expression as `${jasypt::ENC(value)}`. Note that the + encrypted value must be generated using the proper Jasypt encryptor with the same password and algorithm set in + the confguration. + + A possible encrypted value for `12345678` is `ENC(wqp8zDeiCQ5JaFvwDtoAcr2WMLdlD0rjwvo8Rh0thG5qyTQVGxwJjBIiW26y0dtU)` + + Lookups to the configuration `my.secret` will automatically decrypt the value with Jasypt and provide the original + `12345678` string. + +!!! info + + It is possible to generate the encrypted secret with the following [JBang](http://jbang.dev/) script: + + ```shell + jbang https://raw.githubusercontent.com/smallrye/smallrye-config/main/documentation/src/main/docs/config/secret-handlers/jasypt.java -s= -p= + ``` + +##### Configuration + +| Configuration Property | Type | Default | +|--- |--- |--- | +| `smallrye.config.secret-handler.jasypt.password`
The Jasypt password to use | String | | +| `smallrye.config.secret-handler.jasypt.algorithm`
The Jasypt algorithm to use | String | | + +## Secret Keys Names + When configuration properties contain passwords or other kinds of secrets, Smallrye Config can hide them to prevent accidental exposure of such values. **This is no way a replacement for securing secrets.** Proper security mechanisms must still be used to secure -secrets. However, there is still the basic problem that passwords and secrets are generally encoded simply as strings. -Secret Keys provides a way to "lock" the configuration so that secrets do not appear unless explicitly enabled. +secrets. However, there is still the fundamental problem that passwords and secrets are generally encoded simply as +strings. Secret Keys provides a way to "lock" the configuration so that secrets do not appear unless explicitly enabled. -Secret Keys requires the list of the configuration property names that must be hidden. This can be supplied -in `SmallRyeConfigBuilder#withSecretKeys`. +To mark specific keys as secrets, register an instance of `io.smallrye.config.SecretKeysConfigSourceInterceptor` by +using the interceptor factory as follows: ```java -SmallRyeConfig config = new SmallRyeConfigBuilder() - .addDefaultSources() - .addDefaultInterceptors() - .withSources(KeyValuesConfigSource.config(keyValues)) - .withSecretKeys("secret") - .build() +public class SecretKeysConfigInterceptorFactory implements ConfigSourceInterceptorFactory { + @Override + public ConfigSourceInterceptor getInterceptor(ConfigSourceInterceptorContext context) { + return new SecretKeysConfigSourceInterceptor(Set.of("secret")); + } +} ``` +Register the factory so that it can be found at runtime by creating a +`META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file that contains the fully qualified name of +this factory class. + From this point forward, every lookup to the configuration name `secret` will throw a `SecurityException`. -Access to the Secret Keys, is available via the APIs `io.smallrye.config.SecretKeys#doUnlocked(java.lang.Runnable)` +Access the Secret Keys using the APIs `io.smallrye.config.SecretKeys#doUnlocked(java.lang.Runnable)` and `io.smallrye.config.SecretKeys#doUnlocked(java.util.function.Supplier)`. ```java @@ -30,5 +152,5 @@ String secretValue = SecretKeys.doUnlocked(() -> { }); ``` -Secret Keyes are only unlocked in the context of `doUnlocked`. Once the execution completes, the secrets become locked +Secret Keys are only unlocked in the context of `doUnlocked`. Once the execution completes, the secrets become locked again. diff --git a/documentation/src/main/docs/converters/custom.md b/documentation/src/main/docs/converters/custom.md index 1e1da94e8..818aae130 100644 --- a/documentation/src/main/docs/converters/custom.md +++ b/documentation/src/main/docs/converters/custom.md @@ -42,8 +42,7 @@ public class CustomValueConverter implements Converter { And registration in: -_META-INF/services/org.eclipse.microprofile.config.spi.Converter -```properties +```properties title="META-INF/services/org.eclipse.microprofile.config.spi.Converter" org.acme.config.CustomValue ``` @@ -59,14 +58,14 @@ Config config = ConfigProvider.getConfig(); CustomValue value = config.getValue("custom.value", CustomValue.class); ```` -The `javax.annotation.Priority` annotation overrides the `Converter` priority and change converters precedence to fine +The `jakarta.annotation.Priority` annotation overrides the `Converter` priority and change converters precedence to fine tune the execution order. By default, if no `@Priority` is specified by the `Converter`, the converter is registered with a priority of `100`. Consider: ```java package org.acme.config; -import javax.annotation.Priority; +import jakarta.annotation.Priority; import org.eclipse.microprofile.config.spi.Converter; @Priority(150) diff --git a/documentation/src/main/docs/extensions/fallback.md b/documentation/src/main/docs/extensions/fallback.md index 6ac76a50b..2c3ce1a16 100644 --- a/documentation/src/main/docs/extensions/fallback.md +++ b/documentation/src/main/docs/extensions/fallback.md @@ -8,25 +8,41 @@ fall back to provide the same expected behavior. The fallback function is only a configuration name is not found and resolved to the fallback name. ```java -package org.acme; +package org.acme.config; import java.util.function.Function; import io.smallrye.config.FallbackConfigSourceInterceptor; public class MicroProfileConfigFallbackInterceptor extends FallbackConfigSourceInterceptor { public MicroProfileConfigFallbackInterceptor(final Function mapping) { - super(name -> name.startsWith("microprofile.config") ? - name.replaceAll("microprofile\\.config", "smallrye.config") : + super(name -> name.startsWith("mp.config") ? + name.replaceAll("mp\\.config", "smallrye.config") : name); } } ``` And registration in: -_META-INF/services/io.smallrye.config.ConfigSourceInterceptor_ -```properties -io.smallrye.config.MicroProfileConfigFallbackInterceptor + +```properties title="META-INF/services/io.smallrye.config.ConfigSourceInterceptor" +org.acme.config.MicroProfileConfigFallbackInterceptor ``` -The `MicroProfileConfigFallbackInterceptor` can fallback configuration names in the `microprofile.config` namespace +The `MicroProfileConfigFallbackInterceptor` can fallback configuration names in the `mp.config` namespace to the `smallrye.config` namespace. + +!!! example + + ```properties title="application.properties" + mp.config.profile=test + smallrye.config.profile=prod + ``` + + A lookup to `mp.config.profile` returns the value `test`. + + ```properties title="application.properties" + smallrye.config.profile=prod + ``` + + A lookup to `mp.config.profile` returns the value `prod`. The config is not able to find a value for + `mp.config.profile`, so the interceptor fallbacks and lookups the value of `smallrye.config.profile`. diff --git a/documentation/src/main/docs/extensions/interceptors.md b/documentation/src/main/docs/extensions/interceptors.md index 60eab6bdc..bb579c3c4 100644 --- a/documentation/src/main/docs/extensions/interceptors.md +++ b/documentation/src/main/docs/extensions/interceptors.md @@ -26,10 +26,12 @@ origin and ordinal. ```java package org.acme.config; -import javax.annotation.Priority; +import static io.smallrye.config.SecretKeys.doLocked; + +import jakarta.annotation.Priority; import io.smallrye.config.ConfigSourceInterceptor; -import io.smallrye.config.ConfigLogging; +import io.smallrye.config._private.ConfigLogging; @Priority(Priorities.LIBRARY + 200) public class LoggingConfigSourceInterceptor implements ConfigSourceInterceptor { @@ -49,8 +51,8 @@ public class LoggingConfigSourceInterceptor implements ConfigSourceInterceptor { ``` And registration in: -_META-INF/services/io.smallrye.config.ConfigSourceInterceptor_ -```properties + +```properties title="META-INF/services/io.smallrye.config.ConfigSourceInterceptor" org.acme.config.LoggingConfigSourceInterceptor ``` @@ -66,7 +68,7 @@ The `ConfigSourceInterceptorFactory` can initialize an interceptor with access t (so it can be used to configure the interceptor and retrieve configuration values) and set the priority. A `ConfigSourceInterceptor` implementation class can specify a priority by way of the standard -`javax.annotation.Priority` annotation. If no priority is explicitly assigned, the default priority value of +`jakarta.annotation.Priority` annotation. If no priority is explicitly assigned, the default priority value of `io.smallrye.config.Priorities.APPLICATION` is assumed. If multiple interceptors are registered with the same priority, then their execution order may be non-deterministic. diff --git a/documentation/src/main/docs/extensions/logging.md b/documentation/src/main/docs/extensions/logging.md index 880464a8e..0a606a236 100644 --- a/documentation/src/main/docs/extensions/logging.md +++ b/documentation/src/main/docs/extensions/logging.md @@ -1,7 +1,7 @@ ## LoggingConfigSourceInterceptor The `io.smallrye.config.LoggingConfigSourceInterceptor` logs lookups of configuration names in the provided logging -platform. The log information includes config name and value, the config source origing and location if exists. +platform. The log information includes config name and value, the config source origin and location if it exists. The log is done as `debug`, so the debug threshold must be set to `debug` for the `io.smallrye.config` appender to display the logs. diff --git a/documentation/src/main/docs/extensions/relocate.md b/documentation/src/main/docs/extensions/relocate.md index fd13f6076..c0d3e0f8a 100644 --- a/documentation/src/main/docs/extensions/relocate.md +++ b/documentation/src/main/docs/extensions/relocate.md @@ -8,25 +8,35 @@ config sources are not updated yet. The relocation function gives priority to th resolves to the old name if no value is found under the new relocation name. ```java -package org.acme; +package org.acme.config; import java.util.function.Function; import io.smallrye.config.RelocateConfigSourceInterceptor; public class MicroProfileConfigRelocateInterceptor extends RelocateConfigSourceInterceptor { - public MicroProfileConfigFallbackInterceptor(final Function mapping) { - super(name -> name.startsWith("microprofile.config") ? - name.replaceAll("microprofile\\.config", "smallrye.config") : + public MicroProfileConfigRelocateInterceptor(final Function mapping) { + super(name -> name.startsWith("mp.config") ? + name.replaceAll("mp\\.config", "smallrye.config") : name); } } ``` And registration in: -_META-INF/services/io.smallrye.config.ConfigSourceInterceptor_ -```properties -io.smallrye.config.MicroProfileConfigRelocateInterceptor + +```properties title="META-INF/services/io.smallrye.config.ConfigSourceInterceptor" +org.acme.config.MicroProfileConfigRelocateInterceptor ``` -The `MicroProfileConfigRelocateInterceptor` can relocate configuration names in the `microprofile.config` namespace +The `MicroProfileConfigRelocateInterceptor` can relocate configuration names in the `mp.config` namespace to the `smallrye.config` namespace. + +!!! example + + ```properties title="application.properties" + mp.config.profile=test + smallrye.config.profile=prod + ``` + + A lookup to `mp.config.profile` returns the value `prod`. The config finds a valid value in the relocated name + `smallrye.config.profile`, so the interceptor will use this value instead of the one in `mp.config.profile`. diff --git a/documentation/src/main/docs/index.md b/documentation/src/main/docs/index.md index e7582f10d..659c2a518 100644 --- a/documentation/src/main/docs/index.md +++ b/documentation/src/main/docs/index.md @@ -32,19 +32,19 @@ Add the _dependency_ to your project using your preferred build tool: === "Gradle (Groovy)" ```groovy - implementation 'io.smallrye.config:smallrye-config:{{ attributes.versions }}' + implementation 'io.smallrye.config:smallrye-config:{{attributes['version']}}' ``` === "Gradle (Kotlin)" ```kotlin - implementation("io.smallrye.config:smallrye-config:{{ attributes.versions }}") + implementation("io.smallrye.config:smallrye-config:{{attributes['version']}}") ``` === "JBang" ```java - //DEPS io.smallrye.config:smallrye-config:{{ attributes.versions }} + //DEPS io.smallrye.config:smallrye-config:{{attributes['version']}} ``` And retrieve a `SmallRyeConfig` instance with: diff --git a/examples/configmap/pom.xml b/examples/configmap/pom.xml index dae619918..3f0892c48 100644 --- a/examples/configmap/pom.xml +++ b/examples/configmap/pom.xml @@ -4,19 +4,33 @@ io.smallrye smallrye-parent - 35 + 42 + io.smallrye.config.examples configmap - 2.11.1 + 3.7.1 SmallRye Config Examples: ConfigMap true + 2.3.0 + + + + io.smallrye.testing + smallrye-testing-bom + ${version.smallrye.testing} + import + pom + + + + jakarta.annotation @@ -47,7 +61,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.3.0 + 3.5.2 package @@ -55,6 +69,7 @@ shade + false ${project.artifactId}-app @@ -70,7 +85,7 @@ io.fabric8 docker-maven-plugin - 0.40.2 + 0.44.0 true docker-registry:5000 @@ -110,6 +125,6 @@ - 2.11.1 + 3.7.1 diff --git a/examples/expansion/pom.xml b/examples/expansion/pom.xml index d0bcc26e7..66434da10 100644 --- a/examples/expansion/pom.xml +++ b/examples/expansion/pom.xml @@ -20,19 +20,33 @@ io.smallrye smallrye-parent - 35 + 42 + io.smallrye.config.examples expansion - 2.11.1 + 3.7.1 SmallRye Config Examples: Property Expansion true + 2.3.0 + + + + io.smallrye.testing + smallrye-testing-bom + ${version.smallrye.testing} + import + pom + + + + io.smallrye.config @@ -58,6 +72,6 @@ - 2.11.1 + 3.7.1 diff --git a/examples/expansion/src/main/java/io/smallrye/config/examples/expansion/ExampleExpansionBean.java b/examples/expansion/src/main/java/io/smallrye/config/examples/expansion/ExampleExpansionBean.java index 4f5f488af..c444284a8 100644 --- a/examples/expansion/src/main/java/io/smallrye/config/examples/expansion/ExampleExpansionBean.java +++ b/examples/expansion/src/main/java/io/smallrye/config/examples/expansion/ExampleExpansionBean.java @@ -1,7 +1,7 @@ package io.smallrye.config.examples.expansion; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/examples/expansion/src/test/java/io/smallrye/config/examples/expansion/ExampleExpansionBeanTest.java b/examples/expansion/src/test/java/io/smallrye/config/examples/expansion/ExampleExpansionBeanTest.java index 8bf8f5ad9..023796366 100644 --- a/examples/expansion/src/test/java/io/smallrye/config/examples/expansion/ExampleExpansionBeanTest.java +++ b/examples/expansion/src/test/java/io/smallrye/config/examples/expansion/ExampleExpansionBeanTest.java @@ -2,8 +2,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import org.jboss.weld.junit5.WeldInitiator; import org.jboss.weld.junit5.WeldJunit5Extension; diff --git a/examples/interceptors/pom.xml b/examples/interceptors/pom.xml index c7b1aa92b..123ce4be5 100644 --- a/examples/interceptors/pom.xml +++ b/examples/interceptors/pom.xml @@ -20,19 +20,33 @@ io.smallrye smallrye-parent - 35 + 42 + io.smallrye.config.examples interceptors - 2.11.1 + 3.7.1 SmallRye Config Examples: Interceptors true + 2.3.0 + + + + io.smallrye.testing + smallrye-testing-bom + ${version.smallrye.testing} + import + pom + + + + io.smallrye.config @@ -53,6 +67,6 @@ - 2.11.1 + 3.7.1 diff --git a/examples/mapping/pom.xml b/examples/mapping/pom.xml index a33846d84..20ecd19b7 100644 --- a/examples/mapping/pom.xml +++ b/examples/mapping/pom.xml @@ -20,19 +20,33 @@ io.smallrye smallrye-parent - 35 + 42 + io.smallrye.config.examples mapping - 2.11.1 + 3.7.1 SmallRye Config Examples: Config Mapping true + 2.3.0 + + + + io.smallrye.testing + smallrye-testing-bom + ${version.smallrye.testing} + import + pom + + + + io.smallrye.config @@ -58,6 +72,6 @@ - 2.11.1 + 3.7.1 diff --git a/examples/mapping/src/main/java/io/smallrye/config/examples/mapping/MappingSmallRyeConfigFactory.java b/examples/mapping/src/main/java/io/smallrye/config/examples/mapping/MappingSmallRyeConfigFactory.java index 66ff872e4..f4d8e4880 100644 --- a/examples/mapping/src/main/java/io/smallrye/config/examples/mapping/MappingSmallRyeConfigFactory.java +++ b/examples/mapping/src/main/java/io/smallrye/config/examples/mapping/MappingSmallRyeConfigFactory.java @@ -11,6 +11,7 @@ public SmallRyeConfig getConfigFor( return configProviderResolver.getBuilder() .forClassLoader(classLoader) + .addDiscoveredCustomizers() .addDefaultSources() .addDefaultInterceptors() .addDiscoveredSources() diff --git a/examples/mapping/src/main/java/io/smallrye/config/examples/mapping/ServerMapping.java b/examples/mapping/src/main/java/io/smallrye/config/examples/mapping/ServerMapping.java index 1acc03189..3352e6705 100644 --- a/examples/mapping/src/main/java/io/smallrye/config/examples/mapping/ServerMapping.java +++ b/examples/mapping/src/main/java/io/smallrye/config/examples/mapping/ServerMapping.java @@ -6,7 +6,7 @@ public class ServerMapping { public static Server getServer() { - SmallRyeConfig config = (SmallRyeConfig) ConfigProvider.getConfig(); + SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class); return config.getConfigMapping(Server.class); } } diff --git a/examples/mapping/src/main/java/io/smallrye/config/examples/mapping/ServerMappingBean.java b/examples/mapping/src/main/java/io/smallrye/config/examples/mapping/ServerMappingBean.java index 3ee009351..5708cbb9d 100644 --- a/examples/mapping/src/main/java/io/smallrye/config/examples/mapping/ServerMappingBean.java +++ b/examples/mapping/src/main/java/io/smallrye/config/examples/mapping/ServerMappingBean.java @@ -1,7 +1,7 @@ package io.smallrye.config.examples.mapping; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; @ApplicationScoped public class ServerMappingBean { diff --git a/examples/mapping/src/test/java/io/smallrye/config/examples/mapping/ServerMappingBeanTest.java b/examples/mapping/src/test/java/io/smallrye/config/examples/mapping/ServerMappingBeanTest.java index fb6094ad4..717c53456 100644 --- a/examples/mapping/src/test/java/io/smallrye/config/examples/mapping/ServerMappingBeanTest.java +++ b/examples/mapping/src/test/java/io/smallrye/config/examples/mapping/ServerMappingBeanTest.java @@ -6,8 +6,8 @@ import java.time.Duration; import java.util.stream.Stream; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import org.jboss.weld.junit5.WeldInitiator; import org.jboss.weld.junit5.WeldJunit5Extension; diff --git a/examples/pom.xml b/examples/pom.xml index d798f769f..7272bf31a 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -20,7 +20,7 @@ io.smallrye.config smallrye-config-parent - 2.11.1 + 3.7.1 smallrye-config-examples diff --git a/examples/profiles/pom.xml b/examples/profiles/pom.xml index 1d2858323..0d87ce89a 100644 --- a/examples/profiles/pom.xml +++ b/examples/profiles/pom.xml @@ -20,19 +20,33 @@ io.smallrye smallrye-parent - 35 + 42 + io.smallrye.config.examples profiles - 2.11.1 + 3.7.1 SmallRye Config Examples: Profiles true + 2.3.0 + + + + io.smallrye.testing + smallrye-testing-bom + ${version.smallrye.testing} + import + pom + + + + io.smallrye.config @@ -72,6 +86,6 @@ - 2.11.1 + 3.7.1 diff --git a/examples/profiles/src/main/java/io/smallrye/config/examples/profiles/ExampleProfilesBean.java b/examples/profiles/src/main/java/io/smallrye/config/examples/profiles/ExampleProfilesBean.java index 01eea6658..876d5b7da 100644 --- a/examples/profiles/src/main/java/io/smallrye/config/examples/profiles/ExampleProfilesBean.java +++ b/examples/profiles/src/main/java/io/smallrye/config/examples/profiles/ExampleProfilesBean.java @@ -1,7 +1,7 @@ package io.smallrye.config.examples.profiles; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/examples/profiles/src/test/java/io/smallrye/config/examples/profiles/ExampleProfilesBeanTest.java b/examples/profiles/src/test/java/io/smallrye/config/examples/profiles/ExampleProfilesBeanTest.java index 98249c24a..7af94eb8f 100644 --- a/examples/profiles/src/test/java/io/smallrye/config/examples/profiles/ExampleProfilesBeanTest.java +++ b/examples/profiles/src/test/java/io/smallrye/config/examples/profiles/ExampleProfilesBeanTest.java @@ -2,8 +2,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import org.jboss.weld.junit5.WeldInitiator; import org.jboss.weld.junit5.WeldJunit5Extension; diff --git a/implementation/pom.xml b/implementation/pom.xml index cf286f95b..a4fcc9aa1 100644 --- a/implementation/pom.xml +++ b/implementation/pom.xml @@ -20,12 +20,12 @@ io.smallrye.config smallrye-config-parent - 2.11.1 + 3.7.1 smallrye-config-core - SmallRye: MicroProfile Config Core Implementation + SmallRye Config: Core diff --git a/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceFactory.java b/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceFactory.java index c71ef0cf1..847e004da 100644 --- a/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceFactory.java +++ b/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceFactory.java @@ -9,17 +9,19 @@ import org.eclipse.microprofile.config.spi.ConfigSource; -import io.smallrye.common.annotation.Experimental; - /** * This {@code AbstractLocationConfigSourceFactory} allows to initialize additional config locations with the * configuration {@link SmallRyeConfig#SMALLRYE_CONFIG_LOCATIONS}. The configuration support multiple * locations separated by a comma and each must represent a valid {@link URI}. */ -@Experimental("Loads additional config locations") public abstract class AbstractLocationConfigSourceFactory extends AbstractLocationConfigSourceLoader implements ConfigSourceFactory { + @Override + protected boolean failOnMissingFile() { + return true; + } + @Override public Iterable getConfigSources(final ConfigSourceContext context) { final ConfigValue value = context.getValue(SMALLRYE_CONFIG_LOCATIONS); diff --git a/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceLoader.java b/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceLoader.java index fb6ae2687..68c7f09ba 100644 --- a/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceLoader.java +++ b/implementation/src/main/java/io/smallrye/config/AbstractLocationConfigSourceLoader.java @@ -24,7 +24,7 @@ import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.Converter; -import io.smallrye.common.annotation.Experimental; +import io.smallrye.config._private.ConfigMessages; /** * This {@link AbstractLocationConfigSourceLoader} loads {@link ConfigSource}s from a list of specific @@ -46,10 +46,20 @@ * location. This is to keep a consistent loading order and match with the unprofiled resource. Profiles are not * taken into account if the location is a directory. */ -@Experimental("Loads sources by location") public abstract class AbstractLocationConfigSourceLoader { private static final Converter URI_CONVERTER = new URIConverter(); + /** + * If the lookup from an {@link URL} which the scheme {@code file:} should fail. By default, a failed load does not + * throw an exception. In situations where the resource is required, a return value of {@code true} enables the + * exception. + * + * @return {@code true} if file lookup should fail with an exception, {@code false} otherwise. + */ + protected boolean failOnMissingFile() { + return false; + } + /** * The file extensions to filter the locations to load. It does not require to include the dot separator. * @@ -65,16 +75,16 @@ public abstract class AbstractLocationConfigSourceLoader { * @param ordinal the ordinal of the {@link ConfigSource}. * * @return the loaded {@link ConfigSource}. - * @throws IOException if an error occurred when reading from the the {@link URL}. + * @throws IOException if an error occurred when reading from the {@link URL}. */ protected abstract ConfigSource loadConfigSource(final URL url, final int ordinal) throws IOException; protected List loadConfigSources(final String location, final int ordinal) { - return loadConfigSources(new String[] { location }, ordinal); + return loadConfigSources(location != null ? new String[] { location } : null, ordinal); } protected List loadConfigSources(final String location, final int ordinal, final ClassLoader classLoader) { - return loadConfigSources(new String[] { location }, ordinal, classLoader); + return loadConfigSources(location != null ? new String[] { location } : null, ordinal, classLoader); } protected List loadConfigSources(final String[] locations, final int ordinal) { @@ -116,8 +126,10 @@ protected List tryFileSystem(final URI uri, final int ordinal) { addConfigSource(path.toUri(), ordinal, configSources); } } catch (IOException e) { - throw ConfigMessages.msg.failedToLoadResource(e); + throw ConfigMessages.msg.failedToLoadResource(e, uri.toString()); } + } else if ("file".equals(uri.getScheme()) && Files.notExists(urlPath) && failOnMissingFile()) { + throw ConfigMessages.msg.failedToLoadResource(new FileNotFoundException(uri.toString()), uri.toString()); } return configSources; } @@ -128,7 +140,7 @@ protected List tryClassPath(final URI uri, final int ordinal, fina try { consumeAsPaths(useClassloader, uri.getPath(), new ConfigSourcePathConsumer(ordinal, configSources)); } catch (IOException e) { - throw ConfigMessages.msg.failedToLoadResource(e); + throw ConfigMessages.msg.failedToLoadResource(e, uri.toString()); } catch (IllegalArgumentException e) { configSources.addAll(fallbackToUnknownProtocol(uri, ordinal, useClassloader)); } @@ -140,32 +152,34 @@ protected List tryJar(final URI uri, final int ordinal) { try { consumeAsPath(toURL(uri), new ConfigSourcePathConsumer(ordinal, configSources)); } catch (Exception e) { - throw ConfigMessages.msg.failedToLoadResource(e); + throw ConfigMessages.msg.failedToLoadResource(e, uri.toString()); } return configSources; } protected List fallbackToUnknownProtocol(final URI uri, final int ordinal, final ClassLoader classLoader) { - final List configSources = new ArrayList<>(); + List configSources = new ArrayList<>(); try { Enumeration resources = classLoader.getResources(uri.toString()); while (resources.hasMoreElements()) { - final URL resourceUrl = resources.nextElement(); + URL resourceUrl = resources.nextElement(); if (validExtension(resourceUrl.getFile())) { - final ConfigSource mainSource = addConfigSource(resourceUrl, ordinal, configSources); + ConfigSource mainSource = addConfigSource(resourceUrl, ordinal, configSources); configSources.add(new ConfigurableConfigSource((ProfileConfigSourceFactory) profiles -> { - final List profileSources = new ArrayList<>(); + List profileSources = new ArrayList<>(); for (int i = profiles.size() - 1; i >= 0; i--) { - final int mainOrdinal = mainSource.getOrdinal() + profiles.size() - i + 1; - final URI profileUri = addProfileName(uri, profiles.get(i)); - try { - final Enumeration profileResources = classLoader.getResources(profileUri.toString()); - while (profileResources.hasMoreElements()) { - final URL profileUrl = profileResources.nextElement(); - addProfileConfigSource(profileUrl, mainOrdinal, profileSources); + int mainOrdinal = mainSource.getOrdinal() + profiles.size() - i + 1; + for (String fileExtension : getFileExtensions()) { + URI profileUri = addProfileName(uri, profiles.get(i), fileExtension); + try { + Enumeration profileResources = classLoader.getResources(profileUri.toString()); + while (profileResources.hasMoreElements()) { + final URL profileUrl = profileResources.nextElement(); + addProfileConfigSource(profileUrl, mainOrdinal, profileSources); + } + } catch (IOException e) { + // It is ok to not find the resource here, because it is an optional profile resource. } - } catch (IOException e) { - // It is ok to not find the resource here, because it is an optional profile resource. } } return profileSources; @@ -173,7 +187,7 @@ protected List fallbackToUnknownProtocol(final URI uri, final int } } } catch (IOException e) { - throw ConfigMessages.msg.failedToLoadResource(e); + throw ConfigMessages.msg.failedToLoadResource(e, uri.toString()); } return configSources; } @@ -188,15 +202,18 @@ protected List tryHttpResource(final URI uri, final int ordinal) { } protected List tryProfiles(final URI uri, final ConfigSource mainSource) { - final List configSources = new ArrayList<>(); + List configSources = new ArrayList<>(); configSources.add(new ConfigurableConfigSource(new ProfileConfigSourceFactory() { @Override public Iterable getProfileConfigSources(final List profiles) { - final List profileSources = new ArrayList<>(); + List profileSources = new ArrayList<>(); for (int i = profiles.size() - 1; i >= 0; i--) { - final int ordinal = mainSource.getOrdinal() + profiles.size() - i; - final URI profileUri = addProfileName(uri, profiles.get(i)); - AbstractLocationConfigSourceLoader.this.addProfileConfigSource(toURL(profileUri), ordinal, profileSources); + int ordinal = mainSource.getOrdinal() + profiles.size() - i; + for (String fileExtension : getFileExtensions()) { + URI profileUri = addProfileName(uri, profiles.get(i), fileExtension); + AbstractLocationConfigSourceLoader.this.addProfileConfigSource(toURL(profileUri), ordinal, + profileSources); + } } return profileSources; } @@ -227,7 +244,7 @@ private ConfigSource addConfigSource(final URL url, final int ordinal, final Lis configSources.add(configSource); return configSource; } catch (IOException e) { - throw ConfigMessages.msg.failedToLoadResource(e); + throw ConfigMessages.msg.failedToLoadResource(e, url.toString()); } } @@ -238,7 +255,7 @@ private void addProfileConfigSource(final URL profileToFileName, final int ordin } catch (FileNotFoundException | NoSuchFileException e) { // It is ok to not find the resource here, because it is an optional profile resource. } catch (IOException e) { - throw ConfigMessages.msg.failedToLoadResource(e); + throw ConfigMessages.msg.failedToLoadResource(e, profileToFileName.toString()); } } @@ -247,7 +264,13 @@ private boolean validExtension(final Path fileName) { } private boolean validExtension(final String resourceName) { - for (String s : getFileExtensions()) { + String[] fileExtensions = getFileExtensions(); + + if (fileExtensions.length == 0) { + return true; + } + + for (String s : fileExtensions) { if (resourceName.endsWith(s)) { return true; } @@ -255,9 +278,9 @@ private boolean validExtension(final String resourceName) { return false; } - private static URI addProfileName(final URI uri, final String profile) { + private static URI addProfileName(final URI uri, final String profile, final String fileExtension) { if ("jar".equals(uri.getScheme())) { - return URI.create("jar:" + addProfileName(URI.create(decodeIfNeeded(uri).getRawSchemeSpecificPart()), profile)); + return URI.create("jar:" + addProfileName(URI.create(uri.getRawSchemeSpecificPart()), profile, fileExtension)); } final String fileName = uri.getPath(); @@ -266,7 +289,7 @@ private static URI addProfileName(final URI uri, final String profile) { final int dot = fileName.lastIndexOf("."); final String fileNameProfile; if (dot != -1 && dot != 0 && fileName.charAt(dot - 1) != '/') { - fileNameProfile = fileName.substring(0, dot) + "-" + profile + fileName.substring(dot); + fileNameProfile = fileName.substring(0, dot) + "-" + profile + "." + fileExtension; } else { fileNameProfile = fileName + "-" + profile; } @@ -310,19 +333,9 @@ public ConfigSourcePathConsumer(final int ordinal, final List conf public void accept(final Path path) { final AbstractLocationConfigSourceLoader loader = AbstractLocationConfigSourceLoader.this; if (loader.validExtension(path.getFileName().toString())) { - final ConfigSource mainSource = loader.addConfigSource(decodeIfNeeded(path.toUri()), ordinal, configSources); + final ConfigSource mainSource = loader.addConfigSource(path.toUri(), ordinal, configSources); configSources.addAll(loader.tryProfiles(path.toUri(), mainSource)); } } } - - // https://bugs.openjdk.java.net/browse/JDK-8131067 - For Java 8 - @Deprecated - private static URI decodeIfNeeded(final URI uri) { - if (uri.getScheme().equals("jar")) { - return URI.create(uri.getScheme() + ":" + uri.getSchemeSpecificPart()); - } else { - return uri; - } - } } diff --git a/implementation/src/main/java/io/smallrye/config/AbstractMappingConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/AbstractMappingConfigSourceInterceptor.java index 56455a454..17568f402 100644 --- a/implementation/src/main/java/io/smallrye/config/AbstractMappingConfigSourceInterceptor.java +++ b/implementation/src/main/java/io/smallrye/config/AbstractMappingConfigSourceInterceptor.java @@ -1,6 +1,5 @@ package io.smallrye.config; -import java.io.Serializable; import java.util.HashSet; import java.util.Iterator; import java.util.Map; @@ -17,7 +16,12 @@ public AbstractMappingConfigSourceInterceptor(final Function map } public AbstractMappingConfigSourceInterceptor(final Map mappings) { - this((Serializable & Function) name -> mappings.getOrDefault(name, name)); + this(new Function() { + @Override + public String apply(final String name) { + return mappings.getOrDefault(name, name); + } + }); } @Override @@ -35,30 +39,6 @@ public Iterator iterateNames(final ConfigSourceInterceptorContext contex return names.iterator(); } - @Override - public Iterator iterateValues(final ConfigSourceInterceptorContext context) { - final Set values = new HashSet<>(); - final Iterator valuesIterator = context.iterateValues(); - while (valuesIterator.hasNext()) { - final ConfigValue value = valuesIterator.next(); - values.add(value); - final String mappedName = mapping.apply(value.getName()); - if (mappedName != null) { - values.add(ConfigValue.builder() - .withName(mappedName) - .withValue(value.getValue()) - .withRawValue(value.getRawValue()) - .withProfile(value.getProfile()) - .withConfigSourceName(value.getConfigSourceName()) - .withConfigSourcePosition(value.getConfigSourcePosition()) - .withConfigSourceOrdinal(value.getConfigSourceOrdinal()) - .withLineNumber(value.getLineNumber()) - .build()); - } - } - return values.iterator(); - } - protected Function getMapping() { return mapping; } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMapping.java b/implementation/src/main/java/io/smallrye/config/ConfigMapping.java index 027146b67..9805f4802 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMapping.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMapping.java @@ -8,8 +8,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.function.Function; -import io.smallrye.common.annotation.Experimental; +import io.smallrye.config.common.utils.StringUtil; /** * This annotation may be placed in interfaces to group configuration properties with a common prefix. @@ -20,7 +21,7 @@ * @ConfigMapping(prefix = "server") * public interface Server { * public String host(); // maps the property name server.host - * + * * public int port(); // maps to the property name server.port * } * @@ -32,7 +33,6 @@ @Documented @Target({ FIELD, PARAMETER, TYPE }) @Retention(RetentionPolicy.RUNTIME) -@Experimental("ConfigMapping API to group configuration properties") public @interface ConfigMapping { /** * The prefix of the configuration properties. @@ -53,14 +53,28 @@ enum NamingStrategy { /** * The method name is used as is to map the configuration property. */ - VERBATIM, + VERBATIM(name -> name), /** * The method name is derived by replacing case changes with a dash to map the configuration property. */ - KEBAB_CASE, + KEBAB_CASE(name -> { + return StringUtil.skewer(name, '-'); + }), /** * The method name is derived by replacing case changes with an underscore to map the configuration property. */ - SNAKE_CASE + SNAKE_CASE(name -> { + return StringUtil.skewer(name, '_'); + }); + + private final Function function; + + private NamingStrategy(Function function) { + this.function = function; + } + + public String apply(final String name) { + return function.apply(name); + } } } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java index f5b73fe37..384ed5a36 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java @@ -1,176 +1,93 @@ package io.smallrye.config; -import static io.smallrye.config.ConfigMappingInterface.LeafProperty; -import static io.smallrye.config.ConfigMappingInterface.MapProperty; -import static io.smallrye.config.ConfigMappingInterface.PrimitiveProperty; -import static io.smallrye.config.ConfigMappingInterface.Property; -import static io.smallrye.config.ConfigMappingInterface.getConfigurationInterface; -import static io.smallrye.config.ConfigMappingInterface.rawTypeOf; -import static io.smallrye.config.ConfigMappingInterface.typeOfParameter; import static io.smallrye.config.ConfigValidationException.Problem; +import static io.smallrye.config.common.utils.StringUtil.unindexed; +import static java.util.Collections.EMPTY_MAP; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.UndeclaredThrowableException; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.IntFunction; +import java.util.function.Supplier; import org.eclipse.microprofile.config.spi.Converter; -import io.smallrye.config.ConfigMappingInterface.CollectionProperty; -import io.smallrye.config.ConfigMappingInterface.NamingStrategy; +import io.smallrye.config.ConfigMapping.NamingStrategy; +import io.smallrye.config._private.ConfigMessages; /** * A mapping context. This is used by generated classes during configuration mapping, and is released once the configuration * mapping has completed. */ public final class ConfigMappingContext { - private final Map, Map>> enclosedThings = new IdentityHashMap<>(); + private final SmallRyeConfig config; private final Map, Map> roots = new IdentityHashMap<>(); - private final Map, Map>> convertersByTypeAndField = new IdentityHashMap<>(); - private final List, Map>>> keyConvertersByDegreeTypeAndField = new ArrayList<>(); private final Map, Converter> converterInstances = new IdentityHashMap<>(); - private final List allInstances = new ArrayList<>(); - private final SmallRyeConfig config; + + private NamingStrategy namingStrategy; private final StringBuilder stringBuilder = new StringBuilder(); - private final ArrayList problems = new ArrayList<>(); + private final Set usedProperties = new HashSet<>(); + private final List problems = new ArrayList<>(); - private NamingStrategy namingStrategy = null; + private final ConfigMappingNames names; - ConfigMappingContext(final SmallRyeConfig config) { - this.config = config; - } - - public ConfigMappingObject getRoot(Class rootType, String rootPath) { - return roots.getOrDefault(rootType, Collections.emptyMap()).get(rootPath); - } + public ConfigMappingContext( + final SmallRyeConfig config, + final Map>> roots, + final Map>> names) { - public void registerRoot(Class rootType, String rootPath, ConfigMappingObject root) { - roots.computeIfAbsent(rootType, x -> new HashMap<>()).put(rootPath, root); - } + this.config = config; + this.names = new ConfigMappingNames(names); - public Object getEnclosedField(Class enclosingType, String key, Object enclosingObject) { - return enclosedThings - .getOrDefault(enclosingType, Collections.emptyMap()) - .getOrDefault(key, Collections.emptyMap()) - .get(enclosingObject); + for (Map.Entry>> entry : roots.entrySet()) { + String path = entry.getKey(); + for (Class root : entry.getValue()) { + registerRoot(root, path); + } + } } - public void registerEnclosedField(Class enclosingType, String key, Object enclosingObject, Object value) { - enclosedThings - .computeIfAbsent(enclosingType, x -> new HashMap<>()) - .computeIfAbsent(key, x -> new IdentityHashMap<>()) - .put(enclosingObject, value); + private void registerRoot(Class rootType, String rootPath) { + roots.computeIfAbsent(rootType, new Function, Map>() { + @Override + public Map apply(final Class mapping) { + return new HashMap<>(); + } + }).computeIfAbsent(rootPath, new Function() { + @Override + public ConfigMappingObject apply(final String path) { + namingStrategy = null; + stringBuilder.replace(0, stringBuilder.length(), rootPath); + return (ConfigMappingObject) constructRoot(rootType); + } + }); } public T constructRoot(Class interfaceType) { - this.namingStrategy = ConfigMappingInterface.getConfigurationInterface(interfaceType).getNamingStrategy(); return constructGroup(interfaceType); } public T constructGroup(Class interfaceType) { - final T mappingObject = ConfigMappingLoader.configMappingObject(interfaceType, this); - allInstances.add((ConfigMappingObject) mappingObject); + NamingStrategy namingStrategy = this.namingStrategy; + T mappingObject = ConfigMappingLoader.configMappingObject(interfaceType, this); + this.namingStrategy = applyNamingStrategy(namingStrategy); return mappingObject; } - @SuppressWarnings({ "unchecked", "unused" }) - public Converter getValueConverter(Class enclosingType, String field) { - return (Converter) convertersByTypeAndField - .computeIfAbsent(enclosingType, x -> new HashMap<>()) - .computeIfAbsent(field, x -> { - ConfigMappingInterface ci = getConfigurationInterface(enclosingType); - Property property = ci.getProperty(field); - return getConverter(property); - }); - } - - private Converter getConverter(final Property property) { - boolean optional = property.isOptional(); - if (property.isLeaf() || optional && property.asOptional().getNestedProperty().isLeaf()) { - LeafProperty leafProperty = optional ? property.asOptional().getNestedProperty().asLeaf() - : property.asLeaf(); - if (leafProperty.hasConvertWith()) { - Class> convertWith = leafProperty.getConvertWith(); - // todo: generics - return getConverterInstance(convertWith); - } else { - // todo: replace with generic converter lookup - Class valueRawType = leafProperty.getValueRawType(); - if (valueRawType == List.class) { - return config.requireConverter(rawTypeOf(typeOfParameter(leafProperty.getValueType(), 0))); - } else if (valueRawType == Set.class) { - return config.requireConverter(rawTypeOf(typeOfParameter(leafProperty.getValueType(), 0))); - } else { - return config.requireConverter(valueRawType); - } - } - } else if (property.isPrimitive()) { - PrimitiveProperty primitiveProperty = property.asPrimitive(); - if (primitiveProperty.hasConvertWith()) { - return getConverterInstance(primitiveProperty.getConvertWith()); - } else { - return config.requireConverter(primitiveProperty.getBoxType()); - } - } else if (property.isCollection() || optional && property.asOptional().getNestedProperty().isCollection()) { - CollectionProperty collectionProperty = optional ? property.asOptional().getNestedProperty().asCollection() - : property.asCollection(); - return getConverter(collectionProperty.getElement()); - } else { - throw new IllegalStateException(); - } - } - - @SuppressWarnings("unchecked") - public Converter getKeyConverter(Class enclosingType, String field, int degree) { - List, Map>>> list = this.keyConvertersByDegreeTypeAndField; - while (list.size() <= degree) { - list.add(new IdentityHashMap<>()); - } - Map, Map>> map = list.get(degree); - return (Converter) map - .computeIfAbsent(enclosingType, x -> new HashMap<>()) - .computeIfAbsent(field, x -> { - ConfigMappingInterface ci = getConfigurationInterface(enclosingType); - Property property = ci.getProperty(field); - MapProperty mapProperty; - if (property.isMap()) { - mapProperty = property.asMap(); - } else if (property.isCollection()) { - mapProperty = property.asCollection().getElement().asMap(); - } else { - throw new IllegalStateException(); - } - - while (degree + 1 > mapProperty.getLevels()) { - mapProperty = mapProperty.getValueProperty().asMap(); - } - if (mapProperty.hasKeyConvertWith()) { - return getConverterInstance(mapProperty.getKeyConvertWith()); - } else { - // todo: replace with generic converter lookup - Class valueRawType = mapProperty.getKeyRawType(); - if (valueRawType == List.class) { - return Converters.newCollectionConverter( - config.requireConverter(rawTypeOf(typeOfParameter(mapProperty.getKeyType(), 0))), - ArrayList::new); - } else if (valueRawType == Set.class) { - return Converters.newCollectionConverter( - config.requireConverter(rawTypeOf(typeOfParameter(mapProperty.getKeyType(), 0))), - HashSet::new); - } else { - return config.requireConverter(valueRawType); - } - } - }); + @SuppressWarnings("unused") + public ObjectCreator constructObject(String path) { + return new ObjectCreator<>(path); } @SuppressWarnings("unchecked") @@ -196,59 +113,601 @@ public Converter getConverterInstance(Class> createCollectionFactory(final Class type) { - if (type == List.class) { - return ArrayList::new; + @SuppressWarnings("unused") + public void reportProblem(RuntimeException problem) { + problems.add(new Problem(problem.toString())); + } + + List getProblems() { + return problems; + } + + Map, Map> getRootsMap() { + return roots; + } + + void reportUnknown(final List ignoredPaths) { + KeyMap ignoredProperties = new KeyMap<>(); + for (String ignoredPath : ignoredPaths) { + KeyMap found; + if (ignoredPath.endsWith(".**")) { + found = ignoredProperties.findOrAdd(ignoredPath.substring(0, ignoredPath.length() - 3)); + found.putRootValue(Boolean.TRUE); + ignoreRecursively(found); + } else { + if (!ignoredProperties.hasRootValue(ignoredPath)) { + found = ignoredProperties.findOrAdd(ignoredPath); + found.putRootValue(Boolean.TRUE); + } + } } - if (type == Set.class) { - return HashSet::new; + Set roots = new HashSet<>(); + for (Map value : this.roots.values()) { + roots.addAll(value.keySet()); } - throw new IllegalArgumentException(); - } + for (String name : filterPropertiesInRoots(config.getPropertyNames(), roots)) { + if (usedProperties.contains(name)) { + continue; + } - public NoSuchElementException noSuchElement(Class type) { - return new NoSuchElementException("A required configuration group of type " + type.getName() + " was not provided"); + if (!ignoredProperties.hasRootValue(name)) { + ConfigValue configValue = config.getConfigValue(name); + // TODO - https://github.com/quarkusio/quarkus/issues/38479 + if (configValue.getSourceName().startsWith(EnvConfigSource.NAME)) { + continue; + } + problems.add(new Problem( + ConfigMessages.msg.propertyDoesNotMapToAnyRoot(name, configValue.getLocation()))); + } + } } - public void unknownConfigElement(final String propertyName) { - problems.add(new Problem(propertyName + " does not map to any root")); - } + private static void ignoreRecursively(KeyMap root) { + if (root.getRootValue() == null) { + root.putRootValue(Boolean.TRUE); + } - void fillInOptionals() { - for (ConfigMappingObject instance : allInstances) { - instance.fillInOptionals(this); + if (root.getAny() == null) { + //noinspection CollectionAddedToSelf + root.putAny(root); + } else { + var any = root.getAny(); + if (root != any) { + ignoreRecursively(any); + } } - } - public SmallRyeConfig getConfig() { - return config; + for (var value : root.values()) { + ignoreRecursively(value); + } } - public StringBuilder getStringBuilder() { - return stringBuilder; + /** + * Filters the full list of properties names in Config to only the property names that can match any of the + * prefixes (namespaces) registered in mappings. + * + * @param properties the available property names in Config. + * @param roots the registered mapping roots. + * + * @return the property names that match to at least one root. + */ + private static Iterable filterPropertiesInRoots(final Iterable properties, final Set roots) { + if (roots.isEmpty()) { + return properties; + } + + // Will match everything, so no point in filtering + if (roots.contains("")) { + return properties; + } + + List matchedProperties = new ArrayList<>(); + for (String property : properties) { + for (String root : roots) { + if (isPropertyInRoot(property, root)) { + matchedProperties.add(property); + break; + } + } + } + return matchedProperties; } - public void reportProblem(RuntimeException problem) { - problems.add(new Problem(problem.toString())); + private static boolean isPropertyInRoot(final String property, final String root) { + if (property.equals(root)) { + return true; + } + + // if property is less than the root no way to match + if (property.length() <= root.length()) { + return false; + } + + // foo.bar + // foo.bar."baz" + // foo.bar[0] + char c = property.charAt(root.length()); + if ((c == '.') || c == '[') { + return property.startsWith(root); + } + + return false; } - ArrayList getProblems() { - return problems; + @SuppressWarnings("unchecked") + public class ObjectCreator { + private T root; + private List>> creators; + + public ObjectCreator(final String path) { + this.creators = List.of(new Consumer>() { + @Override + public void accept(Function get) { + root = (T) get.apply(path); + } + }); + } + + public ObjectCreator map( + final Class keyRawType, + final Class> keyConvertWith) { + return map(keyRawType, keyConvertWith, null); + } + + public ObjectCreator map( + final Class keyRawType, + final Class> keyConvertWith, + final String unnamedKey) { + return map(keyRawType, keyConvertWith, unnamedKey, (Class) null); + } + + public ObjectCreator map( + final Class keyRawType, + final Class> keyConvertWith, + final String unnamedKey, + final Class defaultClass) { + + Supplier supplier = null; + if (defaultClass != null) { + supplier = new Supplier() { + @Override + public V get() { + int length = stringBuilder.length(); + stringBuilder.append(".*"); + V defaultValue = constructGroup(defaultClass); + stringBuilder.setLength(length); + return defaultValue; + } + }; + } + + return map(keyRawType, keyConvertWith, unnamedKey, supplier); + } + + public ObjectCreator map( + final Class keyRawType, + final Class> keyConvertWith, + final String unnamedKey, + final Supplier defaultValue) { + Converter keyConverter = keyConvertWith == null ? config.requireConverter(keyRawType) + : getConverterInstance(keyConvertWith); + List>> nestedCreators = new ArrayList<>(); + for (Consumer> creator : this.creators) { + creator.accept(new Function() { + @Override + public Object apply(final String path) { + Map map = defaultValue != null ? new MapWithDefault<>(defaultValue.get()) : new HashMap<>(); + + if (unnamedKey != null) { + nestedCreators.add(new Consumer<>() { + @Override + public void accept(Function get) { + V value = (V) get.apply(path); + if (value != null) { + map.put(unnamedKey.equals("") ? null : keyConverter.convert(unnamedKey), value); + } + } + }); + } + + Map mapKeys = new HashMap<>(); + Map> mapProperties = new HashMap<>(); + for (String propertyName : config.getPropertyNames()) { + if (propertyName.length() > path.length() + 1 // only consider properties bigger than the map path + && (path.isEmpty() || propertyName.charAt(path.length()) == '.') // next char must be a dot (for the key) + && propertyName.startsWith(path)) { // the property must start with the map path + + // Start at the map root path + NameIterator mapProperty = !path.isEmpty() + ? new NameIterator(unindexed(propertyName), path.length()) + : new NameIterator(unindexed(propertyName)); + // Move to the next key + mapProperty.next(); + + String mapKey = unindexed(mapProperty.getPreviousSegment()); + mapKeys.computeIfAbsent(mapKey, new Function() { + @Override + public String apply(final String s) { + return unindexed(propertyName.substring(0, mapProperty.getPosition())); + } + }); + + mapProperties.computeIfAbsent(mapKey, new Function>() { + @Override + public List apply(final String s) { + return new ArrayList<>(); + } + }); + mapProperties.get(mapKey).add(propertyName); + } + } + + for (Map.Entry mapKey : mapKeys.entrySet()) { + nestedCreators.add(new Consumer<>() { + @Override + public void accept(Function get) { + // The properties may have been used ih the unnamed key, which cause clashes, so we skip them + if (unnamedKey != null) { + boolean allUsed = true; + for (String mapProperty : mapProperties.get(mapKey.getKey())) { + if (!usedProperties.contains(mapProperty)) { + allUsed = false; + break; + } + } + if (allUsed) { + return; + } + } + + // This is the full path plus the map key + V value = (V) get.apply(mapKey.getValue()); + if (value != null) { + map.put(keyConverter.convert(mapKey.getKey()), value); + } + } + }); + + } + + return map; + } + }); + } + this.creators = nestedCreators; + return this; + } + + public > ObjectCreator collection( + final Class collectionRawType) { + List>> nestedCreators = new ArrayList<>(); + IntFunction> collectionFactory = createCollectionFactory(collectionRawType); + for (Consumer> creator : this.creators) { + Collection collection = (Collection) collectionFactory.apply(0); + creator.accept(new Function() { + @Override + public Object apply(final String path) { + // This is ordered, so it shouldn't require a set by index + for (Integer index : config.getIndexedPropertiesIndexes(path)) { + nestedCreators.add(new Consumer>() { + @Override + public void accept(final Function get) { + collection.add((V) get.apply(path + "[" + index + "]")); + } + }); + } + return collection; + } + }); + } + this.creators = nestedCreators; + return this; + } + + public > ObjectCreator optionalCollection( + final Class collectionRawType) { + List>> nestedCreators = new ArrayList<>(); + IntFunction> collectionFactory = createCollectionFactory(collectionRawType); + for (Consumer> creator : this.creators) { + Collection collection = (Collection) collectionFactory.apply(0); + creator.accept(new Function() { + @Override + public Object apply(final String path) { + // This is ordered, so it shouldn't require a set by index + List indexes = config.getIndexedPropertiesIndexes(path); + for (Integer index : indexes) { + nestedCreators.add(new Consumer>() { + @Override + public void accept(final Function get) { + collection.add((V) get.apply(path + "[" + index + "]")); + } + }); + } + return indexes.isEmpty() ? Optional.empty() : Optional.of(collection); + } + }); + } + this.creators = nestedCreators; + return this; + } + + public ObjectCreator group(final Class groupType) { + for (Consumer> creator : this.creators) { + creator.accept(new Function() { + @Override + public G apply(final String path) { + StringBuilder sb = ConfigMappingContext.this.getStringBuilder(); + int length = sb.length(); + sb.append(path, length, path.length()); + G group = constructGroup(groupType); + sb.setLength(length); + return group; + } + }); + } + return this; + } + + public ObjectCreator lazyGroup(final Class groupType) { + for (Consumer> creator : this.creators) { + creator.accept(new Function() { + @Override + public G apply(final String path) { + if (createRequired(groupType, path)) { + StringBuilder sb = ConfigMappingContext.this.getStringBuilder(); + int length = sb.length(); + sb.append(path, length, path.length()); + G group = constructGroup(groupType); + sb.setLength(length); + return group; + } else { + return null; + } + } + }); + } + return this; + } + + public ObjectCreator optionalGroup(final Class groupType) { + for (Consumer> creator : this.creators) { + creator.accept(new Function() { + @Override + public Optional apply(final String path) { + if (createRequired(groupType, path)) { + StringBuilder sb = ConfigMappingContext.this.getStringBuilder(); + int length = sb.length(); + sb.append(path, length, path.length()); + G group = constructGroup(groupType); + sb.setLength(length); + return Optional.of(group); + } else { + return Optional.empty(); + } + } + }); + } + return this; + } + + public ObjectCreator value( + final Class valueRawType, + final Class> valueConvertWith) { + for (Consumer> creator : creators) { + creator.accept(new Function() { + @Override + public T apply(final String propertyName) { + usedProperties.add(propertyName); + Converter valueConverter = getConverter(valueRawType, valueConvertWith); + return config.getValue(propertyName, valueConverter); + } + }); + } + return this; + } + + public ObjectCreator optionalValue( + final Class valueRawType, + final Class> valueConvertWith) { + for (Consumer> creator : creators) { + creator.accept(new Function() { + @Override + public Optional apply(final String propertyName) { + usedProperties.add(propertyName); + Converter valueConverter = getConverter(valueRawType, valueConvertWith); + return config.getOptionalValue(propertyName, valueConverter); + } + }); + } + return this; + } + + public > ObjectCreator values( + final Class itemRawType, + final Class> itemConvertWith, + final Class collectionRawType) { + for (Consumer> creator : creators) { + creator.accept(new Function() { + @Override + public T apply(final String propertyName) { + usedProperties.add(propertyName); + usedProperties.addAll(config.getIndexedProperties(propertyName)); + Converter itemConverter = itemConvertWith == null ? config.requireConverter(itemRawType) + : getConverterInstance(itemConvertWith); + IntFunction collectionFactory = (IntFunction) createCollectionFactory(collectionRawType); + return (T) config.getValues(propertyName, itemConverter, collectionFactory); + } + }); + } + return this; + } + + public > ObjectCreator optionalValues( + final Class itemRawType, + final Class> itemConvertWith, + final Class collectionRawType) { + for (Consumer> creator : creators) { + creator.accept(new Function() { + @Override + public T apply(final String propertyName) { + usedProperties.add(propertyName); + usedProperties.addAll(config.getIndexedProperties(propertyName)); + Converter itemConverter = getConverter(itemRawType, itemConvertWith); + IntFunction collectionFactory = (IntFunction) createCollectionFactory(collectionRawType); + return (T) config.getOptionalValues(propertyName, itemConverter, collectionFactory); + } + }); + } + return this; + } + + public ObjectCreator values( + final Class keyRawType, + final Class> keyConvertWith, + final Class valueRawType, + final Class> valueConvertWith, + final String defaultValue) { + for (Consumer> creator : creators) { + Function values = new Function<>() { + @Override + public Object apply(final String propertyName) { + usedProperties.add(propertyName); + usedProperties.addAll(config.getMapKeys(propertyName).values()); + Converter keyConverter = getConverter(keyRawType, keyConvertWith); + Converter valueConverter = getConverter(valueRawType, valueConvertWith); + + if (defaultValue == null) { + // TODO - We should use getValues here, but this makes the Map to be required. This is a breaking change + try { + return config.getOptionalValues(propertyName, keyConverter, valueConverter, HashMap::new) + .orElse(EMPTY_MAP); + } catch (NoSuchElementException e) { // Can be thrown by MapConverter, but mappings shouldn't use inline map values + return EMPTY_MAP; + } + } else { + IntFunction> mapFactory = new IntFunction<>() { + @Override + public Map apply(final int value) { + return new MapWithDefault<>(valueConverter.convert(defaultValue)); + } + }; + try { + return config.getOptionalValues(propertyName, keyConverter, valueConverter, mapFactory) + .orElse(mapFactory.apply(0)); + } catch (NoSuchElementException e) { // Can be thrown by MapConverter, but mappings shouldn't use inline map values + return mapFactory.apply(0); + } + } + } + }; + creator.accept(values); + } + return this; + } + + public > ObjectCreator values( + final Class keyRawType, + final Class> keyConvertWith, + final Class valueRawType, + final Class> valueConvertWith, + final Class collectionRawType, + final String defaultValue) { + for (Consumer> creator : creators) { + Function values = new Function<>() { + @Override + public Object apply(final String propertyName) { + usedProperties.add(propertyName); + usedProperties.addAll(config.getMapKeys(propertyName).values()); + Converter keyConverter = getConverter(keyRawType, keyConvertWith); + Converter valueConverter = getConverter(valueRawType, valueConvertWith); + + IntFunction collectionFactory = (IntFunction) createCollectionFactory(collectionRawType); + + if (defaultValue == null) { + // TODO - We should use getValues here, but this makes the Map to be required. This is a breaking change + return config.getOptionalValues(propertyName, keyConverter, valueConverter, HashMap::new, + collectionFactory).orElse(new HashMap<>()); + } else { + IntFunction> mapFactory = new IntFunction<>() { + @Override + public Map apply(final int value) { + return new MapWithDefault<>( + Converters.newCollectionConverter(valueConverter, collectionFactory) + .convert(defaultValue)); + } + }; + + return config.getOptionalValues(propertyName, keyConverter, valueConverter, mapFactory, + collectionFactory).orElse(mapFactory.apply(0)); + } + } + }; + creator.accept(values); + } + return this; + } + + public T get() { + return root; + } + + private Converter getConverter(final Class rawType, final Class> convertWith) { + return convertWith == null ? config.requireConverter(rawType) : getConverterInstance(convertWith); + } + + private boolean createRequired(final Class groupType, final String path) { + Set names = ConfigMappingContext.this.names.get(groupType.getName(), path); + if (names == null) { + return false; + } + + for (String propertyName : config.getPropertyNames()) { + if (propertyName.startsWith(path) && names.contains(new PropertyName(propertyName))) { + return true; + } + } + return false; + } + + private IntFunction> createCollectionFactory(final Class type) { + if (type == List.class) { + return ArrayList::new; + } + + if (type == Set.class) { + return HashSet::new; + } + + throw new IllegalArgumentException(); + } } - Map, Map> getRootsMap() { - return roots; + static class MapWithDefault extends HashMap { + 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 481f7830e..9cde794e3 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingGenerator.java @@ -1,19 +1,22 @@ package io.smallrye.config; import static org.objectweb.asm.Opcodes.ACC_ABSTRACT; +import static org.objectweb.asm.Opcodes.ACC_FINAL; import static org.objectweb.asm.Opcodes.ACC_INTERFACE; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACONST_NULL; import static org.objectweb.asm.Opcodes.ALOAD; import static org.objectweb.asm.Opcodes.ARETURN; import static org.objectweb.asm.Opcodes.ASTORE; import static org.objectweb.asm.Opcodes.CHECKCAST; import static org.objectweb.asm.Opcodes.DUP; import static org.objectweb.asm.Opcodes.GETFIELD; +import static org.objectweb.asm.Opcodes.GETSTATIC; import static org.objectweb.asm.Opcodes.GOTO; -import static org.objectweb.asm.Opcodes.ICONST_0; +import static org.objectweb.asm.Opcodes.I2C; import static org.objectweb.asm.Opcodes.ICONST_1; -import static org.objectweb.asm.Opcodes.IFNE; -import static org.objectweb.asm.Opcodes.IF_ICMPGE; +import static org.objectweb.asm.Opcodes.IFEQ; import static org.objectweb.asm.Opcodes.ILOAD; import static org.objectweb.asm.Opcodes.INVOKEINTERFACE; import static org.objectweb.asm.Opcodes.INVOKESPECIAL; @@ -24,6 +27,7 @@ import static org.objectweb.asm.Opcodes.POP; import static org.objectweb.asm.Opcodes.PUTFIELD; import static org.objectweb.asm.Opcodes.RETURN; +import static org.objectweb.asm.Opcodes.SWAP; import static org.objectweb.asm.Opcodes.V1_8; import static org.objectweb.asm.Type.getDescriptor; import static org.objectweb.asm.Type.getInternalName; @@ -35,20 +39,12 @@ import java.lang.reflect.Modifier; import java.security.AccessController; import java.security.PrivilegedAction; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; import java.util.Set; -import java.util.function.IntFunction; 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; @@ -58,7 +54,11 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; +import io.smallrye.config.ConfigMapping.NamingStrategy; import io.smallrye.config.ConfigMappingInterface.CollectionProperty; +import io.smallrye.config.ConfigMappingInterface.LeafProperty; +import io.smallrye.config.ConfigMappingInterface.MapProperty; +import io.smallrye.config.ConfigMappingInterface.MayBeOptionalProperty; import io.smallrye.config.ConfigMappingInterface.PrimitiveProperty; import io.smallrye.config.ConfigMappingInterface.Property; @@ -75,27 +75,22 @@ public class ConfigMappingGenerator { } private static final String I_CLASS = getInternalName(Class.class); - private static final String I_COLLECTIONS = getInternalName(Collections.class); private static final String I_CONFIGURATION_OBJECT = getInternalName(ConfigMappingObject.class); - private static final String I_CONVERTER = getInternalName(Converter.class); - private static final String I_MAP = getInternalName(Map.class); - private static final String I_COLLECTION = getInternalName(Collection.class); - private static final String I_LIST = getInternalName(List.class); - private static final String I_INT_FUNCTION = getInternalName(IntFunction.class); private static final String I_MAPPING_CONTEXT = getInternalName(ConfigMappingContext.class); + private static final String I_OBJECT_CREATOR = getInternalName(ConfigMappingContext.ObjectCreator.class); private static final String I_OBJECT = getInternalName(Object.class); - private static final String I_OPTIONAL = getInternalName(Optional.class); private static final String I_RUNTIME_EXCEPTION = getInternalName(RuntimeException.class); - private static final String I_SMALLRYE_CONFIG = getInternalName(SmallRyeConfig.class); private static final String I_STRING_BUILDER = getInternalName(StringBuilder.class); - private static final String I_INTEGER = getInternalName(Integer.class); private static final String I_STRING = getInternalName(String.class); + private static final String I_NAMING_STRATEGY = getInternalName(NamingStrategy.class); + private static final String I_SET = getInternalName(Set.class); private static final String I_FIELD = getInternalName(Field.class); private static final int V_THIS = 0; private static final int V_MAPPING_CONTEXT = 1; private static final int V_STRING_BUILDER = 2; private static final int V_LENGTH = 3; + private static final int V_NAMING_STRATEGY = 4; /** * Generates the backing implementation of an interface annotated with the {@link ConfigMapping} annotation. @@ -107,7 +102,7 @@ static byte[] generate(final ConfigMappingInterface mapping) { ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); ClassVisitor visitor = usefulDebugInfo ? new Debugging.ClassVisitorImpl(writer) : writer; - visitor.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, mapping.getClassInternalName(), null, I_OBJECT, + visitor.visit(V1_8, ACC_PUBLIC, mapping.getClassInternalName(), null, I_OBJECT, new String[] { I_CONFIGURATION_OBJECT, getInternalName(mapping.getInterfaceType()) @@ -115,71 +110,68 @@ static byte[] generate(final ConfigMappingInterface mapping) { visitor.visitSource(null, null); // No Args Constructor - To use for proxies - MethodVisitor noArgsCtor = visitor.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); - noArgsCtor.visitVarInsn(Opcodes.ALOAD, V_THIS); - noArgsCtor.visitMethodInsn(Opcodes.INVOKESPECIAL, I_OBJECT, "", "()V", false); + MethodVisitor noArgsCtor = visitor.visitMethod(ACC_PUBLIC, "", "()V", null, null); + noArgsCtor.visitVarInsn(ALOAD, V_THIS); + noArgsCtor.visitMethodInsn(INVOKESPECIAL, I_OBJECT, "", "()V", false); noArgsCtor.visitInsn(RETURN); noArgsCtor.visitEnd(); noArgsCtor.visitMaxs(0, 0); - MethodVisitor ctor = visitor.visitMethod(Opcodes.ACC_PUBLIC, "", "(L" + I_MAPPING_CONTEXT + ";)V", null, null); - ctor.visitParameter("context", Opcodes.ACC_FINAL); + MethodVisitor ctor = visitor.visitMethod(ACC_PUBLIC, "", "(L" + I_MAPPING_CONTEXT + ";)V", null, null); + ctor.visitParameter("context", ACC_FINAL); Label ctorStart = new Label(); Label ctorEnd = new Label(); ctor.visitLabel(ctorStart); // stack: - - ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); + ctor.visitVarInsn(ALOAD, V_THIS); // stack: this - ctor.visitMethodInsn(Opcodes.INVOKESPECIAL, I_OBJECT, "", "()V", false); + ctor.visitMethodInsn(INVOKESPECIAL, I_OBJECT, "", "()V", false); // stack: - - ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT); // stack: ctxt - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getStringBuilder", "()L" + I_STRING_BUILDER + ';', + ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getStringBuilder", "()L" + I_STRING_BUILDER + ';', false); // stack: sb - ctor.visitInsn(Opcodes.DUP); + ctor.visitInsn(DUP); // stack: sb sb Label ctorSbStart = new Label(); ctor.visitLabel(ctorSbStart); - ctor.visitVarInsn(Opcodes.ASTORE, V_STRING_BUILDER); + ctor.visitVarInsn(ASTORE, V_STRING_BUILDER); // stack: sb - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "length", "()I", false); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "length", "()I", false); // stack: len Label ctorLenStart = new Label(); ctor.visitLabel(ctorLenStart); - ctor.visitVarInsn(Opcodes.ISTORE, V_LENGTH); - // stack: - - MethodVisitor fio = visitor.visitMethod(Opcodes.ACC_PUBLIC, "fillInOptionals", "(L" + I_MAPPING_CONTEXT + ";)V", null, - null); - fio.visitParameter("context", Opcodes.ACC_FINAL); - Label fioStart = new Label(); - Label fioEnd = new Label(); - fio.visitLabel(fioStart); - // stack: - - fio.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); - // stack: ctxt - fio.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getStringBuilder", "()L" + I_STRING_BUILDER + ';', - false); - // stack: sb - fio.visitVarInsn(Opcodes.ASTORE, V_STRING_BUILDER); + ctor.visitVarInsn(ISTORE, V_LENGTH); + + Label ctorNsStart = new Label(); + ctor.visitLabel(ctorNsStart); + ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT); + + if (mapping.hasNamingStrategy()) { + ctor.visitFieldInsn(GETSTATIC, I_NAMING_STRATEGY, mapping.getNamingStrategy().name(), + "L" + I_NAMING_STRATEGY + ";"); + } else { + ctor.visitInsn(ACONST_NULL); + } + ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "applyNamingStrategy", + "(L" + I_NAMING_STRATEGY + ";)L" + I_NAMING_STRATEGY + ";", false); + ctor.visitVarInsn(ASTORE, V_NAMING_STRATEGY); + // stack: - - addProperties(visitor, ctor, fio, new HashSet<>(), mapping, mapping.getClassInternalName()); + addProperties(visitor, ctor, new HashSet<>(), mapping, mapping.getClassInternalName()); // stack: - if (mapping.getToStringMethod().generate()) { addToString(visitor, mapping); } // stack: - - fio.visitInsn(Opcodes.RETURN); - fio.visitLabel(fioEnd); - fio.visitLocalVariable("mc", 'L' + I_MAPPING_CONTEXT + ';', null, fioStart, fioEnd, V_MAPPING_CONTEXT); - fio.visitEnd(); - fio.visitMaxs(0, 0); - // stack: - ctor.visitInsn(RETURN); ctor.visitLabel(ctorEnd); ctor.visitLocalVariable("mc", 'L' + I_MAPPING_CONTEXT + ';', null, ctorStart, ctorEnd, V_MAPPING_CONTEXT); ctor.visitLocalVariable("sb", 'L' + I_STRING_BUILDER + ';', null, ctorSbStart, ctorEnd, V_STRING_BUILDER); ctor.visitLocalVariable("len", "I", null, ctorLenStart, ctorEnd, V_LENGTH); + ctor.visitLocalVariable("ns", "Lio/smallrye/config/ConfigMapping$NamingStrategy;", null, ctorNsStart, ctorEnd, + V_NAMING_STRATEGY); ctor.visitEnd(); ctor.visitMaxs(0, 0); visitor.visitEnd(); @@ -216,8 +208,8 @@ static byte[] generate(final Class classType, final String interfaceName) { { AnnotationVisitor av = writer.visitAnnotation("L" + getInternalName(ConfigMapping.class) + ";", true); - av.visitEnum("namingStrategy", "L" + getInternalName(ConfigMapping.NamingStrategy.class) + ";", - ConfigMapping.NamingStrategy.VERBATIM.toString()); + av.visitEnum("namingStrategy", "L" + getInternalName(NamingStrategy.class) + ";", + NamingStrategy.VERBATIM.toString()); if (classType.isAnnotationPresent(ConfigProperties.class)) { av.visit("prefix", classType.getAnnotation(ConfigProperties.class).prefix()); @@ -377,7 +369,6 @@ static byte[] generate(final Class classType, final String interfaceName) { private static void addProperties( final ClassVisitor cv, final MethodVisitor ctor, - final MethodVisitor fio, final Set visited, final ConfigMappingInterface mapping, final String className) { @@ -385,489 +376,299 @@ private static void addProperties( for (Property property : mapping.getProperties()) { Method method = property.getMethod(); String memberName = method.getName(); + + // skip super members with overrides if (!visited.add(memberName)) { - // duplicated property continue; } - // the field + + // Field Declaration String fieldType = getInternalName(method.getReturnType()); String fieldDesc = getDescriptor(method.getReturnType()); - cv.visitField(Opcodes.ACC_PRIVATE, memberName, fieldDesc, null, null); + cv.visitField(ACC_PRIVATE, memberName, fieldDesc, null, null); - // now process the property - final Property realProperty; - final boolean optional = property.isOptional(); - if (optional) { - realProperty = property.asOptional().getNestedProperty(); + // Getter + MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, memberName, "()" + fieldDesc, null, null); + mv.visitVarInsn(ALOAD, V_THIS); + mv.visitFieldInsn(GETFIELD, className, memberName, fieldDesc); + mv.visitInsn(getReturnInstruction(property)); + mv.visitEnd(); + mv.visitMaxs(0, 0); + + if (property.isDefaultMethod()) { + continue; + } + + // Constructor field init + Label _try = new Label(); + Label _catch = new Label(); + Label _continue = new Label(); + ctor.visitTryCatchBlock(_try, _catch, _catch, I_RUNTIME_EXCEPTION); + + appendPropertyName(ctor, property); + ctor.visitVarInsn(ALOAD, V_THIS); + ctor.visitTypeInsn(NEW, I_OBJECT_CREATOR); + ctor.visitInsn(DUP); + ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT); + ctor.visitVarInsn(ALOAD, V_STRING_BUILDER); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "toString", "()L" + I_STRING + ';', false); + ctor.visitMethodInsn(INVOKESPECIAL, I_OBJECT_CREATOR, "", "(L" + I_MAPPING_CONTEXT + ";L" + I_STRING + ";)V", + false); + + // try + ctor.visitLabel(_try); + + generateProperty(ctor, property); + + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "get", "()L" + I_OBJECT + ";", false); + if (property.isPrimitive()) { + PrimitiveProperty primitive = property.asPrimitive(); + ctor.visitTypeInsn(CHECKCAST, getInternalName(primitive.getBoxType())); + ctor.visitMethodInsn(INVOKEVIRTUAL, getInternalName(primitive.getBoxType()), primitive.getUnboxMethodName(), + primitive.getUnboxMethodDescriptor(), false); } else { - realProperty = property; + ctor.visitTypeInsn(CHECKCAST, fieldType); } + ctor.visitFieldInsn(PUTFIELD, className, memberName, fieldDesc); + ctor.visitJumpInsn(GOTO, _continue); - // now handle each possible type - if (property.isCollection() || realProperty.isCollection() && optional) { - ctor.visitVarInsn(ALOAD, V_THIS); - // append property name - boolean restoreLength = appendPropertyName(ctor, property); + // catch + ctor.visitLabel(_catch); + ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT); + ctor.visitInsn(SWAP); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "reportProblem", "(L" + I_RUNTIME_EXCEPTION + ";)V", false); + ctor.visitJumpInsn(GOTO, _continue); - ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getConfig", "()L" + I_SMALLRYE_CONFIG + ';', false); - ctor.visitVarInsn(ALOAD, V_STRING_BUILDER); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "toString", "()L" + I_STRING + ';', false); + ctor.visitLabel(_continue); - // For Both Group and Optional Group - if (realProperty.asCollection().getElement().isGroup() || realProperty.asCollection().getElement().isMap()) { - CollectionProperty collectionProperty = realProperty.asCollection(); + restoreLength(ctor); + } - // get properties indexes - ctor.visitMethodInsn(INVOKEVIRTUAL, I_SMALLRYE_CONFIG, "getIndexedPropertiesIndexes", - "(L" + I_STRING + ";)L" + I_LIST + ';', false); - ctor.visitVarInsn(ASTORE, 4); + // 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()); - // Retrieve Collection to init. - ctor.visitLdcInsn(getType(collectionProperty.getCollectionRawType())); - ctor.visitMethodInsn(INVOKESTATIC, I_MAPPING_CONTEXT, "createCollectionFactory", - "(L" + I_CLASS + ";)L" + I_INT_FUNCTION + ';', false); - ctor.visitVarInsn(ALOAD, 4); - ctor.visitMethodInsn(INVOKEINTERFACE, I_LIST, "size", "()I", true); - ctor.visitMethodInsn(INVOKEINTERFACE, I_INT_FUNCTION, "apply", "(I)L" + I_OBJECT + ";", true); - // We do it in a separate var so we can either PUT directly or wrap it in Optional - ctor.visitVarInsn(ASTORE, 5); - - // TODO - Try to optimize loads / stores / debug - // Iterate - // i = 0 - ctor.visitInsn(ICONST_0); - ctor.visitVarInsn(ISTORE, 6); - // list size - ctor.visitVarInsn(ALOAD, 4); - ctor.visitMethodInsn(INVOKEINTERFACE, I_LIST, "size", "()I", true); - ctor.visitVarInsn(ISTORE, 7); - - Label iter = new Label(); - ctor.visitLabel(iter); - // i - ctor.visitVarInsn(ILOAD, 6); - // size - ctor.visitVarInsn(ILOAD, 7); - Label each = new Label(); - ctor.visitJumpInsn(IF_ICMPGE, each); - - // list to iterate - ctor.visitVarInsn(ALOAD, 4); - // i - ctor.visitVarInsn(ILOAD, 6); - // get property index - ctor.visitMethodInsn(INVOKEINTERFACE, I_LIST, "get", "(I)L" + I_OBJECT + ";", true); - ctor.visitTypeInsn(CHECKCAST, I_INTEGER); - ctor.visitVarInsn(ASTORE, 8); - - // current sb length - ctor.visitVarInsn(ALOAD, V_STRING_BUILDER); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "length", "()I", false); - ctor.visitVarInsn(ISTORE, 9); - - // construct collection index - ctor.visitTypeInsn(NEW, I_STRING_BUILDER); - ctor.visitInsn(DUP); - ctor.visitMethodInsn(INVOKESPECIAL, I_STRING_BUILDER, "", "()V", false); - ctor.visitLdcInsn("["); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", - "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ";", false); - ctor.visitVarInsn(ALOAD, 8); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", - "(L" + I_OBJECT + ";)L" + I_STRING_BUILDER + ";", false); - ctor.visitLdcInsn("]"); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", - "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ";", false); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "toString", "()L" + I_STRING + ";", - false); - ctor.visitVarInsn(ASTORE, 10); - - // append collection index - ctor.visitVarInsn(ALOAD, V_STRING_BUILDER); - ctor.visitVarInsn(ALOAD, 10); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", - "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ";", false); - ctor.visitInsn(POP); - - if (collectionProperty.getElement().isGroup()) { - // create group - ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT); - ctor.visitLdcInsn(getType(collectionProperty.getElement().asGroup().getGroupType().getInterfaceType())); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "constructGroup", - "(L" + I_CLASS + ";)L" + I_OBJECT + ';', false); - ctor.visitVarInsn(ASTORE, 11); - } else if (collectionProperty.getElement().isMap()) { - // create empty map - ctor.visitTypeInsn(NEW, getInternalName(HashMap.class)); - ctor.visitInsn(DUP); - ctor.visitMethodInsn(INVOKESPECIAL, getInternalName(HashMap.class), "", "()V", false); - ctor.visitVarInsn(ASTORE, 11); - } + if (property.isDefaultMethod()) { + ctor.visitVarInsn(ALOAD, V_THIS); + Method defaultMethod = property.asDefaultMethod().getDefaultMethod(); + ctor.visitVarInsn(ALOAD, V_THIS); + ctor.visitMethodInsn(INVOKESTATIC, getInternalName(defaultMethod.getDeclaringClass()), defaultMethod.getName(), + "(" + getType(mapping.getInterfaceType()) + ")" + fieldDesc, false); + ctor.visitFieldInsn(PUTFIELD, className, memberName, fieldDesc); + } + } - // add to collection - ctor.visitVarInsn(ALOAD, 5); - ctor.visitTypeInsn(CHECKCAST, I_COLLECTION); - ctor.visitVarInsn(ALOAD, 11); - ctor.visitMethodInsn(INVOKEINTERFACE, I_COLLECTION, "add", "(L" + I_OBJECT + ";)Z", true); - ctor.visitInsn(POP); - - // register indexed enclosing element - ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); - ctor.visitLdcInsn(Type.getType(mapping.getInterfaceType())); - ctor.visitTypeInsn(NEW, I_STRING_BUILDER); - ctor.visitInsn(DUP); - ctor.visitMethodInsn(INVOKESPECIAL, I_STRING_BUILDER, "", "()V", false); - ctor.visitLdcInsn(memberName); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", - "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ";", false); - ctor.visitVarInsn(ALOAD, 10); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", - "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ";", false); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "toString", "()L" + I_STRING + ";", false); - ctor.visitVarInsn(ALOAD, V_THIS); - ctor.visitVarInsn(ALOAD, 11); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "registerEnclosedField", - "(L" + I_CLASS + ";L" + I_STRING + ";L" + I_OBJECT + ";L" + I_OBJECT + ";)V", - false); - - // reset sb without index - ctor.visitVarInsn(ALOAD, V_STRING_BUILDER); - ctor.visitVarInsn(ILOAD, 9); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "setLength", "(I)V", false); - - // i ++ - ctor.visitIincInsn(6, 1); - ctor.visitJumpInsn(GOTO, iter); - ctor.visitLabel(each); - - // set field value - if (optional) { - ctor.visitVarInsn(ILOAD, 7); - Label optionalEmpty = new Label(); - // If indexed properties are empty, then we couldn't find any element, so Optional.empty. - ctor.visitJumpInsn(IFNE, optionalEmpty); - ctor.visitVarInsn(ALOAD, V_THIS); - ctor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false); - ctor.visitFieldInsn(PUTFIELD, className, memberName, fieldDesc); - Label optionalOf = new Label(); - // Else wrap the Collection in Optional - ctor.visitJumpInsn(GOTO, optionalOf); - ctor.visitLabel(optionalEmpty); - ctor.visitVarInsn(ALOAD, V_THIS); - ctor.visitVarInsn(ALOAD, 5); - - ctor.visitMethodInsn(INVOKESTATIC, I_OPTIONAL, "of", "(L" + I_OBJECT + ";)L" + I_OPTIONAL + ';', false); - ctor.visitFieldInsn(PUTFIELD, className, memberName, fieldDesc); - ctor.visitLabel(optionalOf); - ctor.visitInsn(Opcodes.POP); - } else { - ctor.visitVarInsn(ALOAD, 5); - ctor.visitFieldInsn(PUTFIELD, className, memberName, fieldDesc); - } + for (ConfigMappingInterface superType : mapping.getSuperTypes()) { + addProperties(cv, ctor, visited, superType, className); + } + } - } else if (optional) { - ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); - ctor.visitLdcInsn(getType(mapping.getInterfaceType())); - ctor.visitLdcInsn(memberName); - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getValueConverter", - "(L" + I_CLASS + ";L" + I_STRING + ";)L" + I_CONVERTER + ';', false); - - ctor.visitLdcInsn(getType(realProperty.asCollection().getCollectionRawType())); - ctor.visitMethodInsn(INVOKESTATIC, I_MAPPING_CONTEXT, "createCollectionFactory", - "(L" + I_CLASS + ";)L" + I_INT_FUNCTION + ";", false); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_SMALLRYE_CONFIG, "getOptionalValues", - "(L" + I_STRING + ";L" + I_CONVERTER + ";L" + I_INT_FUNCTION + ";)L" + I_OPTIONAL + ';', false); - ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); - - if (restoreLength) { - restoreLength(ctor); - } + private static void generateProperty(final MethodVisitor ctor, final Property property) { + if (property.isLeaf() || property.isPrimitive() || property.isLeaf() && property.isOptional()) { + Class rawType = property.isLeaf() ? property.asLeaf().getValueRawType() : property.asPrimitive().getBoxType(); + ctor.visitLdcInsn(Type.getType(rawType)); + if (property.hasConvertWith() || property.isLeaf() && property.asLeaf().hasConvertWith()) { + ctor.visitLdcInsn(getType( + property.isLeaf() ? property.asLeaf().getConvertWith() : property.asPrimitive().getConvertWith())); + } else { + ctor.visitInsn(ACONST_NULL); + } + if (property.isOptional()) { + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "optionalValue", + "(L" + I_CLASS + ";L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", false); + } else { + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "value", + "(L" + I_CLASS + ";L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", false); + } + } else if (property.isGroup()) { + ctor.visitLdcInsn(getType(property.asGroup().getGroupType().getInterfaceType())); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "group", "(L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", + false); + } else if (property.isMap()) { + MapProperty mapProperty = property.asMap(); + Property valueProperty = mapProperty.getValueProperty(); + if (valueProperty.isLeaf()) { + ctor.visitLdcInsn(getType(mapProperty.getKeyRawType())); + if (mapProperty.hasKeyConvertWith()) { + ctor.visitLdcInsn(getType(mapProperty.getKeyConvertWith())); } else { - ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); - ctor.visitLdcInsn(getType(mapping.getInterfaceType())); - ctor.visitLdcInsn(memberName); - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getValueConverter", - "(L" + I_CLASS + ";L" + I_STRING + ";)L" + I_CONVERTER + ';', false); - - ctor.visitLdcInsn(getType(fieldDesc)); - ctor.visitMethodInsn(INVOKESTATIC, I_MAPPING_CONTEXT, "createCollectionFactory", - "(L" + I_CLASS + ";)L" + I_INT_FUNCTION + ";", false); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_SMALLRYE_CONFIG, "getValues", - "(L" + I_STRING + ";L" + I_CONVERTER + ";L" + I_INT_FUNCTION + ";)L" + I_COLLECTION + ';', false); - ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); + ctor.visitInsn(ACONST_NULL); } - - // reset stringbuilder - if (restoreLength) { - restoreLength(ctor); + LeafProperty leafProperty = valueProperty.asLeaf(); + ctor.visitLdcInsn(getType(leafProperty.getValueRawType())); + if (leafProperty.hasConvertWith()) { + ctor.visitLdcInsn(getType(leafProperty.getConvertWith())); + } else { + ctor.visitInsn(ACONST_NULL); } - - } else if (property.isMap()) { - // stack: - - ctor.visitMethodInsn(Opcodes.INVOKESTATIC, I_COLLECTIONS, "emptyMap", "()L" + I_MAP + ';', false); - // stack: map - ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); - // stack: map this - ctor.visitInsn(Opcodes.SWAP); - // stack: this map - ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); - // stack: - - // then sweep it up - // stack: - - fio.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); - // stack: ctxt - fio.visitLdcInsn(getType(mapping.getInterfaceType())); - // stack: ctxt iface - fio.visitLdcInsn(memberName); - // stack: ctxt iface name - fio.visitVarInsn(Opcodes.ALOAD, V_THIS); - // stack: ctxt iface name this - fio.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getEnclosedField", - "(L" + I_CLASS + ";L" + I_STRING + ";L" + I_OBJECT + ";)L" + I_OBJECT + ';', false); - // stack: obj? - fio.visitInsn(Opcodes.DUP); - Label _continue = new Label(); - Label _done = new Label(); - // stack: obj? obj? - fio.visitJumpInsn(Opcodes.IFNULL, _continue); - // stack: obj - fio.visitTypeInsn(Opcodes.CHECKCAST, I_MAP); - // stack: map - fio.visitVarInsn(Opcodes.ALOAD, V_THIS); - // stack: map this - fio.visitInsn(Opcodes.SWAP); - // stack: this map - fio.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); - // stack: - - fio.visitJumpInsn(Opcodes.GOTO, _done); - fio.visitLabel(_continue); - // stack: null - fio.visitInsn(Opcodes.POP); - // stack: - - fio.visitLabel(_done); - } else if (property.isGroup()) { - // stack: - - boolean restoreLength = appendPropertyName(ctor, property); - // stack: - - ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); - // stack: ctxt - ctor.visitLdcInsn(getType(realProperty.asGroup().getGroupType().getInterfaceType())); - // stack: ctxt clazz - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "constructGroup", - "(L" + I_CLASS + ";)L" + I_OBJECT + ';', false); - // stack: nested - ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); - // stack: nested this - ctor.visitInsn(Opcodes.SWAP); - // stack: this nested - ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); - // register the group - ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); - ctor.visitLdcInsn(Type.getType(mapping.getInterfaceType())); - ctor.visitLdcInsn(memberName); - ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); - ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); - ctor.visitFieldInsn(GETFIELD, className, memberName, fieldDesc); - ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "registerEnclosedField", - "(L" + I_CLASS + ";L" + I_STRING + ";L" + I_OBJECT + ";L" + I_OBJECT + ";)V", - false); - // stack: - - if (restoreLength) { - restoreLength(ctor); + if (mapProperty.hasDefaultValue() && mapProperty.getDefaultValue() != null) { + ctor.visitLdcInsn(mapProperty.getDefaultValue()); + } else { + ctor.visitInsn(ACONST_NULL); + } + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "values", "(L" + I_CLASS + ";L" + I_CLASS + ";L" + I_CLASS + + ";L" + I_CLASS + ";L" + I_STRING + ";)L" + I_OBJECT_CREATOR + ";", false); + } else if (valueProperty.isGroup()) { + ctor.visitLdcInsn(getType(mapProperty.getKeyRawType())); + if (mapProperty.hasKeyConvertWith()) { + ctor.visitLdcInsn(getType(mapProperty.getKeyConvertWith())); + } else { + ctor.visitInsn(ACONST_NULL); + } + if (mapProperty.hasKeyUnnamed()) { + ctor.visitLdcInsn(mapProperty.getKeyUnnamed()); + } else { + ctor.visitInsn(ACONST_NULL); } - } else if (property.isLeaf() || property.isPrimitive() || property.isOptional() && property.isLeaf()) { - // stack: - - ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); - // stack: this - boolean restoreLength = appendPropertyName(ctor, property); - ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getConfig", "()L" + I_SMALLRYE_CONFIG + ';', + if (mapProperty.hasDefaultValue()) { + ctor.visitLdcInsn(getType(valueProperty.asGroup().getGroupType().getInterfaceType())); + } else { + ctor.visitInsn(ACONST_NULL); + } + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "map", + "(L" + I_CLASS + ";L" + I_CLASS + ";L" + I_STRING + ";L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", false); - // stack: this config - ctor.visitVarInsn(Opcodes.ALOAD, V_STRING_BUILDER); - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "toString", "()L" + I_STRING + ';', false); - // stack: this config key - // get the converter to use - ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); - ctor.visitLdcInsn(getType(mapping.getInterfaceType())); - ctor.visitLdcInsn(memberName); - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getValueConverter", - "(L" + I_CLASS + ";L" + I_STRING + ";)L" + I_CONVERTER + ';', false); - // stack: this config key converter - Label _try = new Label(); - Label _catch = new Label(); - Label _continue = new Label(); - ctor.visitLabel(_try); - if (property.isOptional()) { - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_SMALLRYE_CONFIG, "getOptionalValue", - "(L" + I_STRING + ";L" + I_CONVERTER + ";)L" + I_OPTIONAL + ';', false); + ctor.visitLdcInsn(getType(valueProperty.asGroup().getGroupType().getInterfaceType())); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "lazyGroup", + "(L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", false); + } else if (valueProperty.isCollection() && valueProperty.asCollection().getElement().isLeaf()) { + ctor.visitLdcInsn(getType(mapProperty.getKeyRawType())); + if (mapProperty.hasKeyConvertWith()) { + ctor.visitLdcInsn(getType(mapProperty.getKeyConvertWith())); } else { - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_SMALLRYE_CONFIG, "getValue", - "(L" + I_STRING + ";L" + I_CONVERTER + ";)L" + I_OBJECT + ';', false); + ctor.visitInsn(ACONST_NULL); } - // stack: this value - if (property.isPrimitive()) { - PrimitiveProperty prim = property.asPrimitive(); - // unbox it - // stack: this box - String boxType = getInternalName(prim.getBoxType()); - ctor.visitTypeInsn(Opcodes.CHECKCAST, boxType); - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, boxType, prim.getUnboxMethodName(), - prim.getUnboxMethodDescriptor(), false); - // stack: this value - } else if (!property.isOptional()) { - assert property.isLeaf(); - ctor.visitTypeInsn(Opcodes.CHECKCAST, fieldType); + LeafProperty leafProperty = mapProperty.getValueProperty().asCollection().getElement().asLeaf(); + ctor.visitLdcInsn(getType(leafProperty.getValueRawType())); + if (leafProperty.hasConvertWith()) { + ctor.visitLdcInsn(getType(leafProperty.getConvertWith())); + } else { + ctor.visitInsn(ACONST_NULL); } - // stack: this value - ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); - // stack: - - ctor.visitJumpInsn(Opcodes.GOTO, _continue); - ctor.visitLabel(_catch); - // stack: exception - ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); - // stack: exception ctxt - ctor.visitInsn(Opcodes.SWAP); - // stack: ctxt exception - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "reportProblem", - "(L" + I_RUNTIME_EXCEPTION + ";)V", false); - // stack: - - ctor.visitLabel(_continue); - if (restoreLength) { - restoreLength(ctor); + ctor.visitLdcInsn(getType(mapProperty.getValueProperty().asCollection().getCollectionRawType())); + if (mapProperty.hasDefaultValue()) { + ctor.visitLdcInsn(mapProperty.getDefaultValue()); + } else { + ctor.visitInsn(ACONST_NULL); } - // add the try/catch - ctor.visitTryCatchBlock(_try, _catch, _catch, I_RUNTIME_EXCEPTION); - } else if (property.isOptional()) { - // stack: - - ctor.visitMethodInsn(Opcodes.INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false); - // stack: empty - ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); - // stack: empty this - ctor.visitInsn(Opcodes.SWAP); - // stack: this empty - ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); - - // also generate a sweep-up stub - // stack: - - fio.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); - // stack: ctxt - fio.visitLdcInsn(getType(mapping.getInterfaceType())); - // stack: ctxt iface - fio.visitLdcInsn(memberName); - // stack: ctxt iface name - fio.visitVarInsn(Opcodes.ALOAD, V_THIS); - // stack: ctxt iface name this - fio.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getEnclosedField", - "(L" + I_CLASS + ";L" + I_STRING + ";L" + I_OBJECT + ";)L" + I_OBJECT + ';', false); - // stack: obj? - fio.visitInsn(Opcodes.DUP); - Label _continue = new Label(); - Label _done = new Label(); - // stack: obj? obj? - fio.visitJumpInsn(Opcodes.IFNULL, _continue); - // stack: obj - fio.visitMethodInsn(Opcodes.INVOKESTATIC, I_OPTIONAL, "of", "(L" + I_OBJECT + ";)L" + I_OPTIONAL + ';', false); - // stack: opt - fio.visitVarInsn(Opcodes.ALOAD, V_THIS); - // stack: opt this - fio.visitInsn(Opcodes.SWAP); - // stack: this opt - fio.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); - // stack: - - fio.visitJumpInsn(Opcodes.GOTO, _done); - fio.visitLabel(_continue); - // stack: null - fio.visitInsn(Opcodes.POP); - // stack: - - fio.visitLabel(_done); - } else if (property.isDefaultMethod()) { - // Call default methods in fillInOptionals. - // 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. - fio.visitVarInsn(Opcodes.ALOAD, V_THIS); - Method defaultMethod = property.asDefaultMethod().getDefaultMethod(); - fio.visitVarInsn(Opcodes.ALOAD, V_THIS); - fio.visitMethodInsn(INVOKESTATIC, getInternalName(defaultMethod.getDeclaringClass()), defaultMethod.getName(), - "(" + getType(mapping.getInterfaceType()) + ")" + fieldDesc, false); - fio.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "values", "(L" + I_CLASS + ";L" + I_CLASS + ";L" + I_CLASS + + ";L" + I_CLASS + ";L" + I_CLASS + ";L" + I_STRING + ";)L" + I_OBJECT_CREATOR + ";", false); + } else { + unwrapProperty(ctor, property); + } + } else if (property.isCollection()) { + CollectionProperty collectionProperty = property.asCollection(); + if (collectionProperty.getElement().isLeaf()) { + ctor.visitLdcInsn(getType(collectionProperty.getElement().asLeaf().getValueRawType())); + if (collectionProperty.getElement().hasConvertWith()) { + ctor.visitLdcInsn(getType(collectionProperty.getElement().asLeaf().getConvertWith())); + } else { + ctor.visitInsn(ACONST_NULL); + } + ctor.visitLdcInsn(getType(collectionProperty.getCollectionRawType())); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "values", + "(L" + I_CLASS + ";L" + I_CLASS + ";L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", false); + } else { + unwrapProperty(ctor, property); + } + } else if (property.isOptional()) { + final MayBeOptionalProperty nestedProperty = property.asOptional().getNestedProperty(); + if (nestedProperty.isGroup()) { + ctor.visitLdcInsn(getType(nestedProperty.asGroup().getGroupType().getInterfaceType())); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "optionalGroup", + "(L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", false); + } else if (nestedProperty.isCollection()) { + CollectionProperty collectionProperty = nestedProperty.asCollection(); + if (collectionProperty.getElement().isLeaf()) { + ctor.visitLdcInsn(getType(collectionProperty.getElement().asLeaf().getValueRawType())); + if (collectionProperty.getElement().hasConvertWith()) { + ctor.visitLdcInsn(getType(collectionProperty.getElement().asLeaf().getConvertWith())); + } else { + ctor.visitInsn(ACONST_NULL); + } + ctor.visitLdcInsn(getType(collectionProperty.getCollectionRawType())); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "optionalValues", + "(L" + I_CLASS + ";L" + I_CLASS + ";L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", false); + } else { + ctor.visitLdcInsn(getType(collectionProperty.getCollectionRawType())); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "optionalCollection", + "(L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", false); + generateProperty(ctor, collectionProperty.getElement()); + } + } else { + throw new UnsupportedOperationException(); } - - // the accessor method implementation - MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, memberName, "()" + fieldDesc, null, null); - // stack: - - mv.visitVarInsn(Opcodes.ALOAD, V_THIS); - // stack: this - mv.visitFieldInsn(GETFIELD, className, memberName, fieldDesc); - // stack: obj - mv.visitInsn(getReturnInstruction(property)); - - mv.visitEnd(); - mv.visitMaxs(0, 0); - // end loop + } else { + throw new UnsupportedOperationException(); } - // subtype overrides supertype - for (ConfigMappingInterface superType : mapping.getSuperTypes()) { - addProperties(cv, ctor, fio, visited, superType, className); + } + + private static void unwrapProperty(final MethodVisitor ctor, final Property property) { + if (property.isMap()) { + MapProperty mapProperty = property.asMap(); + ctor.visitLdcInsn(getType(mapProperty.getKeyRawType())); + if (mapProperty.hasKeyConvertWith()) { + ctor.visitLdcInsn(getType(mapProperty.getKeyConvertWith())); + } else { + ctor.visitInsn(ACONST_NULL); + } + if (mapProperty.hasKeyUnnamed()) { + ctor.visitLdcInsn(mapProperty.getKeyUnnamed()); + } else { + ctor.visitInsn(ACONST_NULL); + } + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "map", + "(L" + I_CLASS + ";L" + I_CLASS + ";L" + I_STRING + ";)L" + I_OBJECT_CREATOR + ";", false); + generateProperty(ctor, mapProperty.getValueProperty()); + } else if (property.isCollection()) { + CollectionProperty collectionProperty = property.asCollection(); + ctor.visitLdcInsn(getType(collectionProperty.getCollectionRawType())); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_OBJECT_CREATOR, "collection", "(L" + I_CLASS + ";)L" + I_OBJECT_CREATOR + ";", + false); + generateProperty(ctor, collectionProperty.getElement()); + } else { + throw new UnsupportedOperationException(); } } - private static boolean appendPropertyName(final MethodVisitor ctor, final Property property) { + private static void appendPropertyName(final MethodVisitor ctor, final Property property) { if (property.isParentPropertyName()) { - return false; + return; } - // stack: - - Label _continue = new Label(); - ctor.visitVarInsn(Opcodes.ALOAD, V_STRING_BUILDER); - - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "length", "()I", false); - // if length != 0 (mean that a prefix exists and not the empty prefix) - ctor.visitJumpInsn(Opcodes.IFEQ, _continue); + Label _continue = new Label(); + ctor.visitVarInsn(ALOAD, V_STRING_BUILDER); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "length", "()I", false); + ctor.visitJumpInsn(IFEQ, _continue); - ctor.visitVarInsn(Opcodes.ALOAD, V_STRING_BUILDER); - // stack: sb + ctor.visitVarInsn(ALOAD, V_STRING_BUILDER); ctor.visitLdcInsn('.'); - // stack: sb '.' - ctor.visitInsn(Opcodes.I2C); - // stack: sb '.' - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "append", "(C)L" + I_STRING_BUILDER + ';', false); - - ctor.visitInsn(Opcodes.POP); - + ctor.visitInsn(I2C); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", "(C)L" + I_STRING_BUILDER + ';', false); + ctor.visitInsn(POP); ctor.visitLabel(_continue); - ctor.visitVarInsn(Opcodes.ALOAD, V_STRING_BUILDER); - + ctor.visitVarInsn(ALOAD, V_STRING_BUILDER); if (property.hasPropertyName()) { ctor.visitLdcInsn(property.getPropertyName()); } else { - ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); - + ctor.visitVarInsn(ALOAD, V_NAMING_STRATEGY); ctor.visitLdcInsn(property.getPropertyName()); - - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, - "applyNamingStrategy", "(L" + I_STRING + ";)L" + I_STRING + ";", false); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_NAMING_STRATEGY, "apply", "(L" + I_STRING + ";)L" + I_STRING + ";", false); } - // stack: sb name - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "append", - "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ';', false); - // stack: sb - ctor.visitInsn(Opcodes.POP); - // stack: - - return true; + ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ';', + false); + ctor.visitInsn(POP); } private static void restoreLength(final MethodVisitor ctor) { - // stack: - - ctor.visitVarInsn(Opcodes.ALOAD, V_STRING_BUILDER); - // stack: sb - ctor.visitVarInsn(Opcodes.ILOAD, V_LENGTH); - // stack: sb length - ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "setLength", "(I)V", false); - // stack: - + ctor.visitVarInsn(ALOAD, V_STRING_BUILDER); + ctor.visitVarInsn(ILOAD, V_LENGTH); + ctor.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "setLength", "(I)V", false); } private static int getReturnInstruction(Property property) { @@ -917,7 +718,7 @@ private static void addToString(final ClassVisitor visitor, final ConfigMappingI ts.visitLdcInsn(mapping.getInterfaceType().getSimpleName() + "{"); ts.visitMethodInsn(INVOKEVIRTUAL, I_STRING_BUILDER, "append", "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ";", false); - Property[] properties = mapping.getProperties(); + Property[] properties = mapping.getProperties(true); for (int i = 0, propertiesLength = properties.length; i < propertiesLength; i++) { Property property = properties[i]; if (property.isDefaultMethod()) { diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java index c27076938..f5ef46f0a 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java @@ -1,6 +1,7 @@ package io.smallrye.config; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.AnnotatedParameterizedType; import java.lang.reflect.AnnotatedType; import java.lang.reflect.Array; import java.lang.reflect.GenericArrayType; @@ -10,17 +11,20 @@ import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import org.eclipse.microprofile.config.spi.Converter; import io.smallrye.common.constraint.Assert; -import io.smallrye.config.common.utils.StringUtil; +import io.smallrye.config.ConfigMapping.NamingStrategy; +import io.smallrye.config._private.ConfigMessages; /** * Information about a configuration interface. @@ -38,8 +42,6 @@ protected ConfigMappingInterface computeValue(final Class type) { private final String className; private final ConfigMappingInterface[] superTypes; private final Property[] properties; - private final Map propertiesByName; - private final NamingStrategy namingStrategy; private final ToStringMethod toStringMethod; ConfigMappingInterface(final Class interfaceType, final ConfigMappingInterface[] superTypes, @@ -49,12 +51,10 @@ protected ConfigMappingInterface computeValue(final Class type) { this.superTypes = superTypes; List filteredProperties = new ArrayList<>(); - Map propertiesByName = new HashMap<>(); ToStringMethod toStringMethod = null; for (Property property : properties) { if (!property.isToStringMethod()) { filteredProperties.add(property); - propertiesByName.put(property.getMethod().getName(), property); } else { toStringMethod = (ToStringMethod) property; } @@ -64,8 +64,6 @@ protected ConfigMappingInterface computeValue(final Class type) { } this.properties = filteredProperties.toArray(new Property[0]); - this.propertiesByName = propertiesByName; - this.namingStrategy = getNamingStrategy(interfaceType); this.toStringMethod = toStringMethod; } @@ -89,68 +87,44 @@ public Class getInterfaceType() { return interfaceType; } - /** - * Get the number of supertypes which define configuration properties. Implemented interfaces which do not - * define any configuration properties and whose supertypes in turn do not define any configuration properties - * are not counted. - * - * @return the number of supertypes - */ - int getSuperTypeCount() { - return superTypes.length; - } - - ConfigMappingInterface[] getSuperTypes() { + public ConfigMappingInterface[] getSuperTypes() { return superTypes; } - /** - * Get the supertype at the given index, which must be greater than or equal to zero and less than the value returned - * by {@link #getSuperTypeCount()}. - * - * @param index the index - * @return the supertype definition - * @throws IndexOutOfBoundsException if {@code index} is invalid - */ - ConfigMappingInterface getSuperType(int index) throws IndexOutOfBoundsException { - if (index < 0 || index >= superTypes.length) - throw new IndexOutOfBoundsException(); - return superTypes[index]; - } - public Property[] getProperties() { return properties; } - /** - * Get the number of properties defined on this type (excluding supertypes). - * - * @return the number of properties - */ - int getPropertyCount() { - return properties.length; + public Property[] getProperties(boolean includeSuper) { + if (includeSuper) { + Map properties = getSuperProperties(this); + for (Property property : this.properties) { + properties.put(property.getMemberName(), property); + } + return properties.values().toArray(new Property[0]); + } else { + return getProperties(); + } } - /** - * Get the property definition at the given index, which must be greater than or equal to zero and less than the - * value returned by {@link #getPropertyCount()}. - * - * @param index the index - * @return the property definition - * @throws IndexOutOfBoundsException if {@code index} is invalid - */ - Property getProperty(int index) throws IndexOutOfBoundsException { - if (index < 0 || index >= properties.length) - throw new IndexOutOfBoundsException(); - return properties[index]; + private static Map getSuperProperties(ConfigMappingInterface type) { + Map properties = new HashMap<>(); + for (ConfigMappingInterface superType : type.getSuperTypes()) { + properties.putAll(getSuperProperties(superType)); + for (Property property : superType.getProperties()) { + properties.put(property.getMemberName(), property); + } + } + return properties; } - Property getProperty(final String name) { - return propertiesByName.get(name); + public boolean hasNamingStrategy() { + return interfaceType.getAnnotation(ConfigMapping.class) != null; } public NamingStrategy getNamingStrategy() { - return namingStrategy; + ConfigMapping configMapping = interfaceType.getAnnotation(ConfigMapping.class); + return configMapping != null ? configMapping.namingStrategy() : NamingStrategy.KEBAB_CASE; } ToStringMethod getToStringMethod() { @@ -166,9 +140,9 @@ String getClassInternalName() { } List getNested() { - ArrayList nested = new ArrayList<>(); + Set nested = new LinkedHashSet<>(); getNested(properties, nested); - return nested; + return new ArrayList<>(nested); } public byte[] getClassBytes() { @@ -195,14 +169,34 @@ public String getPropertyName() { return hasPropertyName() && !propertyName.isEmpty() ? propertyName : method.getName(); } + public String getPropertyName(final NamingStrategy namingStrategy) { + return hasPropertyName() ? getPropertyName() : namingStrategy.apply(getPropertyName()); + } + + public String getMemberName() { + return method.getName(); + } + public boolean hasPropertyName() { return propertyName != null; } + public boolean hasConvertWith() { + return false; + } + public boolean isParentPropertyName() { return hasPropertyName() && propertyName.isEmpty(); } + public boolean hasDefaultValue() { + return false; + } + + public String getDefaultValue() { + return null; + } + public boolean isPrimitive() { return false; } @@ -270,6 +264,42 @@ public CollectionProperty asCollection() { public DefaultMethodProperty asDefaultMethod() { throw new ClassCastException(); } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Property property = (Property) o; + boolean result = method.equals(property.method) && propertyName.equals(property.propertyName); + if (result) { + return result; + } + return isMethodInHierarchy(property.getMethod().getDeclaringClass(), method); + } + + @Override + public int hashCode() { + return Objects.hash(method, propertyName); + } + + private static boolean isMethodInHierarchy(final Class declaringClass, final Method method) { + for (Class parent : declaringClass.getInterfaces()) { + for (final Method parentMethod : parent.getMethods()) { + if (parentMethod.getName().equals(method.getName())) { + return true; + } + } + boolean methodInHierarchy = isMethodInHierarchy(parent, method); + if (methodInHierarchy) { + return true; + } + } + return false; + } } public static abstract class MayBeOptionalProperty extends Property { @@ -425,6 +455,21 @@ public boolean isLeaf() { return nestedProperty.isLeaf(); } + @Override + public LeafProperty asLeaf() { + return isLeaf() ? nestedProperty.asLeaf() : super.asLeaf(); + } + + @Override + public boolean hasDefaultValue() { + return isLeaf() && nestedProperty.asLeaf().hasDefaultValue(); + } + + @Override + public String getDefaultValue() { + return hasDefaultValue() ? nestedProperty.asLeaf().getDefaultValue() : null; + } + public MayBeOptionalProperty getNestedProperty() { return nestedProperty; } @@ -451,6 +496,14 @@ public boolean isGroup() { public GroupProperty asGroup() { return this; } + + public boolean hasNamingStrategy() { + return groupType.getInterfaceType().isAnnotationPresent(ConfigMapping.class); + } + + public NamingStrategy getNamingStrategy() { + return groupType.getNamingStrategy(); + } } public static final class LeafProperty extends MayBeOptionalProperty { @@ -464,7 +517,7 @@ public static final class LeafProperty extends MayBeOptionalProperty { super(method, propertyName); this.valueType = valueType; this.convertWith = convertWith; - rawType = rawTypeOf(valueType); + this.rawType = rawTypeOf(valueType); this.defaultValue = defaultValue; } @@ -505,15 +558,29 @@ public LeafProperty asLeaf() { public static final class MapProperty extends Property { private final Type keyType; + private final String keyUnnamed; private final Class> keyConvertWith; private final Property valueProperty; + private final boolean hasDefault; + private final String defaultValue; + + MapProperty( + final Method method, + final String propertyName, + final Type keyType, + final String keyUnnamed, + final Class> keyConvertWith, + final Property valueProperty, + final boolean hasDefault, + final String defaultValue) { - MapProperty(final Method method, final String propertyName, final Type keyType, - final Class> keyConvertWith, final Property valueProperty) { super(method, propertyName); this.keyType = keyType; + this.keyUnnamed = keyUnnamed; this.keyConvertWith = keyConvertWith; this.valueProperty = valueProperty; + this.hasDefault = hasDefault; + this.defaultValue = defaultValue; } public Type getKeyType() { @@ -524,6 +591,14 @@ public Class getKeyRawType() { return rawTypeOf(keyType); } + public String getKeyUnnamed() { + return keyUnnamed; + } + + public boolean hasKeyUnnamed() { + return keyUnnamed != null; + } + public Class> getKeyConvertWith() { return Assert.checkNotNullParam("keyConvertWith", keyConvertWith); } @@ -536,6 +611,14 @@ public Property getValueProperty() { return valueProperty; } + public String getDefaultValue() { + return defaultValue; + } + + public boolean hasDefaultValue() { + return hasDefault; + } + @Override public boolean isMap() { return true; @@ -545,14 +628,6 @@ public boolean isMap() { public MapProperty asMap() { return this; } - - public int getLevels() { - if (valueProperty.isMap()) { - return valueProperty.asMap().getLevels() + 1; - } else { - return 1; - } - } } public static final class CollectionProperty extends MayBeOptionalProperty { @@ -678,31 +753,33 @@ private static ConfigMappingInterface[] getSuperTypes(Class[] interfaces, int } static Property[] getProperties(Method[] methods, int si, int ti) { - if (si == methods.length) { - if (ti == 0) { - return NO_PROPERTIES; + for (int i = si; i < methods.length; i++) { + Method method = methods[i]; + int mods = method.getModifiers(); + if (!Modifier.isPublic(mods) || Modifier.isStatic(mods) || !Modifier.isAbstract(mods)) { + // no need for recursive calls here, which are costy in interpreted mode! + continue; + } + if (method.getParameterCount() > 0) { + throw new IllegalArgumentException("Configuration methods cannot accept parameters"); + } + if (method.getReturnType() == void.class) { + throw new IllegalArgumentException("Void config methods are not allowed"); + } + Property p = getPropertyDef(method, method.getAnnotatedReturnType()); + final Property[] array; + if (i + 1 == methods.length) { + array = new Property[ti + 1]; } else { - return new Property[ti]; + array = getProperties(methods, i + 1, ti + 1); } + array[ti] = p; + return array; } - Method method = methods[si]; - int mods = method.getModifiers(); - if (!Modifier.isPublic(mods) || Modifier.isStatic(mods) || !Modifier.isAbstract(mods)) { - return getProperties(methods, si + 1, ti); - } - if (method.getParameterCount() > 0) { - throw new IllegalArgumentException("Configuration methods cannot accept parameters"); - } - if (method.getReturnType() == void.class) { - throw new IllegalArgumentException("Void config methods are not allowed"); - } - Property p = getPropertyDef(method, method.getGenericReturnType()); - Property[] array = getProperties(methods, si + 1, ti + 1); - array[ti] = p; - return array; + return ti > 0 ? new Property[ti] : NO_PROPERTIES; } - private static Property getPropertyDef(Method method, Type type) { + private static Property getPropertyDef(Method method, AnnotatedType type) { if (isToStringMethod(method)) { return new ToStringMethod(method); } @@ -713,20 +790,12 @@ private static Property getPropertyDef(Method method, Type type) { } // now figure out what kind it is - Class> convertWith = getConvertWith(type); - if (convertWith == null) { - WithConverter withConverter = method.getAnnotation(WithConverter.class); - if (withConverter != null) { - convertWith = withConverter.value(); - } - } + Class> convertWith = getConverter(type, method); String propertyName = getPropertyName(method); - Class rawType = rawTypeOf(type); + Class rawType = rawTypeOf(type.getType()); if (rawType.isPrimitive()) { // primitive! - WithDefault annotation = method.getAnnotation(WithDefault.class); - return new PrimitiveProperty(method, propertyName, rawType, convertWith, - annotation == null ? null : annotation.value()); + return new PrimitiveProperty(method, propertyName, rawType, convertWith, getDefaultValue(method)); } if (convertWith == null) { if (rawType == Optional.class) { @@ -739,26 +808,31 @@ private static Property getPropertyDef(Method method, Type type) { } if (rawType == Map.class) { // it's a map... - Type keyType = typeOfParameter(type, 0); - Class> keyConvertWith = getConvertWith(keyType); - Type valueType = typeOfParameter(type, 1); - return new MapProperty(method, propertyName, keyType, keyConvertWith, getPropertyDef(method, valueType)); + AnnotatedType keyType = typeOfParameter(type, 0); + AnnotatedType valueType = typeOfParameter(type, 1); + String defaultValue = getDefaultValue(method); + return new MapProperty(method, propertyName, keyType.getType(), getUnnamedKey(keyType, method), + getConverter(keyType, method), getPropertyDef(method, valueType), + defaultValue != null || hasDefaults(method), defaultValue); } if (rawType == List.class || rawType == Set.class) { - Type elementType = typeOfParameter(type, 0); + AnnotatedType elementType = typeOfParameter(type, 0); - if (rawTypeOf(elementType) == Map.class) { + if (rawTypeOf(elementType.getType()) == Map.class) { return new CollectionProperty(rawType, getPropertyDef(method, elementType)); } - ConfigMappingInterface configurationInterface = getConfigurationInterface((Class) elementType); + ConfigMappingInterface configurationInterface = getConfigurationInterface(rawTypeOf(elementType.getType())); if (configurationInterface != null) { return new CollectionProperty(rawType, new GroupProperty(method, propertyName, configurationInterface)); } - WithDefault annotation = method.getAnnotation(WithDefault.class); - return new CollectionProperty(rawType, new LeafProperty(method, propertyName, elementType, null, - annotation == null ? null : annotation.value())); + Class> converter = getConverter(elementType, method); + if (converter != null) { + convertWith = converter; + } + return new CollectionProperty(rawType, + new LeafProperty(method, propertyName, elementType.getType(), convertWith, getDefaultValue(method))); } ConfigMappingInterface configurationInterface = getConfigurationInterface(rawType); if (configurationInterface != null) { @@ -768,17 +842,22 @@ private static Property getPropertyDef(Method method, Type type) { // fall out (leaf) } + String defaultValue = getDefaultValue(method); if (rawType == List.class || rawType == Set.class) { - Type elementType = typeOfParameter(type, 0); - WithDefault annotation = method.getAnnotation(WithDefault.class); + AnnotatedType elementType = typeOfParameter(type, 0); + Class> converter = getConverter(elementType, method); + if (converter != null) { + convertWith = converter; + } return new CollectionProperty(rawType, - new LeafProperty(method, propertyName, elementType, convertWith, - annotation == null ? null : annotation.value())); + new LeafProperty(method, propertyName, elementType.getType(), convertWith, defaultValue)); + } else if (rawType == Optional.class) { + return new OptionalProperty(method, propertyName, + new LeafProperty(method, propertyName, type.getType(), convertWith, defaultValue)); } // otherwise it's a leaf - WithDefault annotation = method.getAnnotation(WithDefault.class); - return new LeafProperty(method, propertyName, type, convertWith, annotation == null ? null : annotation.value()); + return new LeafProperty(method, propertyName, type.getType(), convertWith, defaultValue); } private static boolean isToStringMethod(Method method) { @@ -809,25 +888,59 @@ private static Method hasDefaultMethodImplementation(Method method) { return null; } - private static Class> getConvertWith(final Type type) { - if (type instanceof AnnotatedType) { - WithConverter annotation = ((AnnotatedType) type).getAnnotation(WithConverter.class); - if (annotation != null) { - return annotation.value(); - } else { - return null; - } + private static String getDefaultValue(final Method method) { + WithDefault annotation = method.getAnnotation(WithDefault.class); + return annotation == null ? null : annotation.value(); + } + + private static boolean hasDefaults(final Method method) { + return method.getAnnotation(WithDefaults.class) != null; + } + + private static String getUnnamedKey(final AnnotatedType type, final Method method) { + WithUnnamedKey annotation = type.getAnnotation(WithUnnamedKey.class); + if (annotation == null) { + annotation = method.getAnnotation(WithUnnamedKey.class); + } + return annotation != null ? annotation.value() : null; + } + + private static Class> getConverter(final AnnotatedType type, final Method method) { + WithConverter annotation = type.getAnnotation(WithConverter.class); + // fallback to method + if (annotation == null) { + annotation = method.getAnnotation(WithConverter.class); + } + if (annotation != null) { + Class> value = annotation.value(); + validateConverter(type.getType(), value); + return value; } else { return null; } } + private static void validateConverter(final Type type, final Class> convertWith) { + if (type instanceof Class) { + try { + Class classType = (Class) type; + Class effectiveType = classType.isPrimitive() ? PrimitiveProperty.boxTypes.get(classType) : classType; + Method convertMethod = convertWith.getMethod("convert", String.class); + if (!effectiveType.isAssignableFrom(convertMethod.getReturnType())) { + throw new IllegalArgumentException(); + } + } catch (NoSuchMethodException e) { + // Ignore + } + } + } + private static String getPropertyName(final AnnotatedElement element) { boolean useParent = element.getAnnotation(WithParentName.class) != null; WithName annotation = element.getAnnotation(WithName.class); if (annotation != null) { if (useParent) { - throw new IllegalArgumentException("Cannot specify both @ParentConfigName and @ConfigName"); + throw new IllegalArgumentException("Cannot specify both @WithParentName and @WithName in '" + element + "'"); } String name = annotation.value(); if (!name.isEmpty()) { @@ -843,13 +956,14 @@ private static String getPropertyName(final AnnotatedElement element) { } } - private static void getNested(final Property[] properties, final List nested) { + private static void getNested(final Property[] properties, final Set nested) { for (Property property : properties) { if (property instanceof GroupProperty) { GroupProperty groupProperty = (GroupProperty) property; ConfigMappingInterface group = groupProperty.getGroupType(); nested.add(group); - getNested(group.properties, nested); + Collections.addAll(nested, group.superTypes); + getNested(group.getProperties(true), nested); } if (property instanceof OptionalProperty) { @@ -858,7 +972,8 @@ private static void getNested(final Property[] properties, final List rawTypeOf(final Type type) { throw ConfigMessages.msg.noRawType(type); } } - - private static NamingStrategy getNamingStrategy(final Class interfaceType) { - final ConfigMapping configMapping = interfaceType.getAnnotation(ConfigMapping.class); - if (configMapping != null) { - switch (configMapping.namingStrategy()) { - case VERBATIM: - return VERBATIM_NAMING_STRATEGY; - case KEBAB_CASE: - return KEBAB_CASE_NAMING_STRATEGY; - case SNAKE_CASE: - return SNAKE_CASE_NAMING_STRATEGY; - } - } - - return DEFAULT_NAMING_STRATEGY; - } - - private static final NamingStrategy DEFAULT_NAMING_STRATEGY = new KebabNamingStrategy(); - private static final NamingStrategy VERBATIM_NAMING_STRATEGY = new VerbatimNamingStrategy(); - private static final NamingStrategy KEBAB_CASE_NAMING_STRATEGY = new KebabNamingStrategy(); - private static final NamingStrategy SNAKE_CASE_NAMING_STRATEGY = new SnakeNamingStrategy(); - - public interface NamingStrategy extends Function { - default boolean isDefault() { - return this.equals(DEFAULT_NAMING_STRATEGY); - } - } - - static class VerbatimNamingStrategy implements NamingStrategy { - @Override - public String apply(final String s) { - return s; - } - } - - static class KebabNamingStrategy implements NamingStrategy { - @Override - public String apply(final String s) { - return StringUtil.skewer(s, '-'); - } - } - - static class SnakeNamingStrategy implements NamingStrategy { - @Override - public String apply(final String s) { - return StringUtil.skewer(s, '_'); - } - } } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingLoader.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingLoader.java index 9e1bf6c99..0ecae4d3e 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingLoader.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingLoader.java @@ -11,6 +11,7 @@ import org.eclipse.microprofile.config.inject.ConfigProperties; import io.smallrye.common.classloader.ClassDefiner; +import io.smallrye.config._private.ConfigMessages; public final class ConfigMappingLoader { private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); @@ -23,9 +24,9 @@ protected ConfigMappingObjectHolder computeValue(final Class type) { } }; - public static List getConfigMappingsMetadata(Class type) { - final List mappings = new ArrayList<>(); - final ConfigMappingInterface configurationInterface = ConfigMappingInterface.getConfigurationInterface(type); + public static List getConfigMappingsMetadata(final Class type) { + List mappings = new ArrayList<>(); + ConfigMappingInterface configurationInterface = ConfigMappingInterface.getConfigurationInterface(type); if (configurationInterface != null) { mappings.add(configurationInterface); mappings.addAll(configurationInterface.getNested()); @@ -34,15 +35,15 @@ public static List getConfigMappingsMetadata(Class typ mappings.addAll(superType.getNested()); } } - final ConfigMappingClass configMappingClass = ConfigMappingClass.getConfigurationClass(type); + ConfigMappingClass configMappingClass = ConfigMappingClass.getConfigurationClass(type); if (configMappingClass != null) { mappings.add(configMappingClass); - mappings.addAll(getConfigMappingsMetadata(getConfigMappingInterface(type).getInterfaceType())); + mappings.addAll(getConfigMappingsMetadata(getConfigMapping(type).getInterfaceType())); } - return mappings; + return List.copyOf(mappings); } - static ConfigMappingInterface getConfigMappingInterface(final Class type) { + public static ConfigMappingInterface getConfigMapping(final Class type) { return ConfigMappingInterface.getConfigurationInterface(getConfigMappingClass(type)); } @@ -57,7 +58,7 @@ static Class getConfigMappingClass(final Class type) { } } - static T configMappingObject(Class interfaceType, ConfigMappingContext configMappingContext) { + static T configMappingObject(final Class interfaceType, final ConfigMappingContext configMappingContext) { ConfigMappingObject instance; try { Constructor constructor = CACHE.get(interfaceType).getImplementationClass() @@ -82,12 +83,25 @@ static T configMappingObject(Class interfaceType, ConfigMappingContext co } @SuppressWarnings("unchecked") - public static Class getImplementationClass(Class type) { - final ConfigMappingMetadata mappingMetadata = ConfigMappingInterface.getConfigurationInterface(type); - if (mappingMetadata == null) { - throw ConfigMessages.msg.classIsNotAMapping(type); + public static Class getImplementationClass(final Class type) { + try { + Class implementationClass = type.getClassLoader().loadClass(type.getName() + type.getName().hashCode() + "Impl"); + if (type.isAssignableFrom(implementationClass)) { + return (Class) implementationClass; + } + + ConfigMappingMetadata mappingMetadata = ConfigMappingInterface.getConfigurationInterface(type); + if (mappingMetadata == null) { + throw ConfigMessages.msg.classIsNotAMapping(type); + } + return (Class) loadClass(type, mappingMetadata); + } catch (ClassNotFoundException e) { + ConfigMappingMetadata mappingMetadata = ConfigMappingInterface.getConfigurationInterface(type); + if (mappingMetadata == null) { + throw ConfigMessages.msg.classIsNotAMapping(type); + } + return (Class) loadClass(type, mappingMetadata); } - return (Class) loadClass(type, mappingMetadata); } static Class loadClass(final Class parent, final ConfigMappingMetadata configMappingMetadata) { @@ -95,7 +109,7 @@ static Class loadClass(final Class parent, final ConfigMappingMetadata con synchronized (getClassLoaderLock(configMappingMetadata.getClassName())) { // Check if the interface implementation was already loaded. If not we will load it. try { - final Class klass = parent.getClassLoader().loadClass(configMappingMetadata.getClassName()); + Class klass = parent.getClassLoader().loadClass(configMappingMetadata.getClassName()); // Check if this is the right classloader class. If not we will load it. if (parent.isAssignableFrom(klass)) { return klass; @@ -132,7 +146,7 @@ private static Class defineClass(final Class parent, final String classNam return ClassDefiner.defineClass(LOOKUP, parent, className, classBytes); } - private static Object getClassLoaderLock(String className) { + private static Object getClassLoaderLock(final String className) { return classLoaderLocks.computeIfAbsent(className, c -> new Object()); } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingNames.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingNames.java new file mode 100644 index 000000000..01e3e1e65 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingNames.java @@ -0,0 +1,51 @@ +package io.smallrye.config; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +class ConfigMappingNames { + private final Map names; + + public ConfigMappingNames(final Map>> names) { + this.names = new HashMap<>(names.size()); + for (Map.Entry>> entry : names.entrySet()) { + this.names.put(entry.getKey(), new Names(entry.getValue())); + } + } + + Set get(String mapping, String name) { + return names.get(mapping).get(name); + } + + private static class Names { + private final Map> names; + private final Map> anys; + + Names(Map> names) { + this.names = new HashMap<>(); + this.anys = new HashMap<>(); + for (Map.Entry> entry : names.entrySet()) { + if (entry.getKey().indexOf('*') == -1) { + this.names.put(new PropertyName(entry.getKey()), toMappingNameSet(entry.getValue())); + } else { + this.anys.put(new PropertyName(entry.getKey()), toMappingNameSet(entry.getValue())); + } + } + } + + Set get(String name) { + PropertyName mappingName = new PropertyName(name); + return names.getOrDefault(mappingName, anys.get(mappingName)); + } + + private static Set toMappingNameSet(Set names) { + Set mappingNames = new HashSet<>(names.size()); + for (String name : names) { + mappingNames.add(new PropertyName(name)); + } + return mappingNames; + } + } +} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingObject.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingObject.java index daa710847..184c6864d 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingObject.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingObject.java @@ -4,5 +4,4 @@ * An interface implemented internally by configuration object implementations. */ public interface ConfigMappingObject { - void fillInOptionals(ConfigMappingContext context); } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java index cba76abc4..e2b76d964 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java @@ -1,36 +1,31 @@ package io.smallrye.config; -import static io.smallrye.config.ConfigMappingInterface.GroupProperty; -import static io.smallrye.config.ConfigMappingInterface.LeafProperty; -import static io.smallrye.config.ConfigMappingInterface.MapProperty; -import static io.smallrye.config.ConfigMappingInterface.PrimitiveProperty; -import static io.smallrye.config.ConfigMappingInterface.Property; import static io.smallrye.config.ConfigMappingLoader.getConfigMappingClass; -import static io.smallrye.config.ConfigMappingLoader.getConfigMappingInterface; +import static io.smallrye.config.ConfigMappings.getDefaults; +import static io.smallrye.config.ConfigMappings.getNames; +import static io.smallrye.config.ConfigMappings.getProperties; +import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; +import static io.smallrye.config.ProfileConfigSourceInterceptor.*; import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN; import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores; -import static java.lang.Integer.parseInt; +import static io.smallrye.config.common.utils.StringUtil.toLowerCaseAndDotted; import java.io.Serializable; -import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.IntFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; import org.eclipse.microprofile.config.spi.ConfigSource; -import org.eclipse.microprofile.config.spi.Converter; import io.smallrye.common.constraint.Assert; -import io.smallrye.common.function.Functions; -import io.smallrye.config.ConfigMappingInterface.CollectionProperty; -import io.smallrye.config.ConfigMappingInterface.NamingStrategy; +import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix; /** * @@ -38,1155 +33,305 @@ final class ConfigMappingProvider implements Serializable { private static final long serialVersionUID = 3977667610888849912L; - /** - * The do-nothing action is used when the matched property is eager. - */ - private static final BiConsumer DO_NOTHING = Functions.discardingBiConsumer(); - private static final KeyMap> IGNORE_EVERYTHING; - - static { - final KeyMap> map = new KeyMap<>(); - map.putRootValue(DO_NOTHING); - IGNORE_EVERYTHING = map; - } - private final Map>> roots; - private final KeyMap> matchActions; - private final Map properties; - private final KeyMap defaultValues; + private final Map>> names; + private final List ignoredPaths; private final boolean validateUnknown; ConfigMappingProvider(final Builder builder) { this.roots = new HashMap<>(builder.roots); - this.matchActions = new KeyMap<>(); - this.properties = new HashMap<>(); - this.defaultValues = new KeyMap<>(); + this.names = builder.names; + this.ignoredPaths = builder.ignoredPaths; this.validateUnknown = builder.validateUnknown; - - final ArrayDeque currentPath = new ArrayDeque<>(); - for (Map.Entry>> entry : roots.entrySet()) { - NameIterator rootNi = new NameIterator(entry.getKey()); - while (rootNi.hasNext()) { - final String nextSegment = rootNi.getNextSegment(); - if (!nextSegment.isEmpty()) { - currentPath.add(nextSegment); - } - rootNi.next(); - } - List> roots = entry.getValue(); - for (Class root : roots) { - // construct the lazy match actions for each group - BiFunction ef = new GetRootAction(root, - entry.getKey()); - ConfigMappingInterface mapping = getConfigMappingInterface(root); - processEagerGroup(currentPath, matchActions, defaultValues, mapping.getNamingStrategy(), mapping, ef); - } - currentPath.clear(); - } - for (String[] ignoredPath : builder.ignored) { - int len = ignoredPath.length; - KeyMap> found; - if (ignoredPath[len - 1].equals("**")) { - found = matchActions.findOrAdd(ignoredPath, 0, len - 1); - found.putRootValue(DO_NOTHING); - found.putAny(IGNORE_EVERYTHING); - } else { - found = matchActions.findOrAdd(ignoredPath); - found.putRootValue(DO_NOTHING); - } - } - } - - static final class ConsumeOneAndThen implements BiConsumer { - private final BiConsumer delegate; - - ConsumeOneAndThen(final BiConsumer delegate) { - this.delegate = delegate; - } - - public void accept(final ConfigMappingContext context, final NameIterator nameIterator) { - nameIterator.previous(); - delegate.accept(context, nameIterator); - nameIterator.next(); - } } - static final class ConsumeOneAndThenFn implements BiFunction { - private final BiFunction delegate; - - ConsumeOneAndThenFn(final BiFunction delegate) { - this.delegate = delegate; - } - - public T apply(final ConfigMappingContext context, final NameIterator nameIterator) { - nameIterator.previous(); - T result = delegate.apply(context, nameIterator); - nameIterator.next(); - return result; - } - } - - private void processEagerGroup( - final ArrayDeque currentPath, - final KeyMap> matchActions, - final KeyMap defaultValues, - final NamingStrategy namingStrategy, - final ConfigMappingInterface group, - final BiFunction getEnclosingFunction) { - - Class type = group.getInterfaceType(); - HashSet usedProperties = new HashSet<>(); - for (int i = 0; i < group.getPropertyCount(); i++) { - Property property = group.getProperty(i); - String memberName = property.getMethod().getName(); - ArrayDeque propertyPath = new ArrayDeque<>(currentPath); - if (usedProperties.add(memberName)) { - // process by property type - if (!property.isParentPropertyName()) { - NameIterator ni = new NameIterator(property.hasPropertyName() ? property.getPropertyName() - : propertyName(property, group, namingStrategy)); - while (ni.hasNext()) { - propertyPath.add(ni.getNextSegment()); - ni.next(); - } - } - processProperty(propertyPath, matchActions, defaultValues, namingStrategy, group, getEnclosingFunction, type, - memberName, property); - } - } - int sc = group.getSuperTypeCount(); - for (int i = 0; i < sc; i++) { - processEagerGroup(currentPath, matchActions, defaultValues, namingStrategy, group.getSuperType(i), - getEnclosingFunction); - } + public static Builder builder() { + return new Builder(); } - private void processProperty( - final ArrayDeque currentPath, - final KeyMap> matchActions, - final KeyMap defaultValues, - final NamingStrategy namingStrategy, - final ConfigMappingInterface group, - final BiFunction getEnclosingFunction, - final Class type, - final String memberName, - final Property property) { + Map, Map> mapConfiguration(final SmallRyeConfig config) + throws ConfigValidationException { + // Register additional dissabiguation property names comparing mapped keys and env names + matchPropertiesWithEnv(config); - if (property.isOptional()) { - // switch to lazy mode - Property nestedProperty = property.asOptional().getNestedProperty(); - processOptionalProperty(currentPath, matchActions, defaultValues, namingStrategy, group, getEnclosingFunction, type, - memberName, nestedProperty); - } else if (property.isGroup()) { - processEagerGroup(currentPath, matchActions, defaultValues, namingStrategy, property.asGroup().getGroupType(), - new GetOrCreateEnclosingGroupInGroup(getEnclosingFunction, group, property.asGroup(), currentPath)); - } else if (property.isPrimitive()) { - // already processed eagerly - PrimitiveProperty primitiveProperty = property.asPrimitive(); - if (primitiveProperty.hasDefaultValue()) { - defaultValues.findOrAdd(currentPath).putRootValue(primitiveProperty.getDefaultValue()); - // collections may also be represented without [] so we need to register both paths - if (isCollection(currentPath)) { - defaultValues.findOrAdd(inlineCollectionPath(currentPath)) - .putRootValue(primitiveProperty.getDefaultValue()); - } - } - addAction(currentPath, property, DO_NOTHING); - // collections may also be represented without [] so we need to register both paths - if (isCollection(currentPath)) { - addAction(inlineCollectionPath(currentPath), property, DO_NOTHING); - } - } else if (property.isLeaf()) { - // already processed eagerly - LeafProperty leafProperty = property.asLeaf(); - if (leafProperty.hasDefaultValue()) { - defaultValues.findOrAdd(currentPath).putRootValue(leafProperty.getDefaultValue()); - // collections may also be represented without [] so we need to register both paths - if (isCollection(currentPath)) { - defaultValues.findOrAdd(inlineCollectionPath(currentPath)).putRootValue(leafProperty.getDefaultValue()); - } - } - // ignore with no error message - addAction(currentPath, property, DO_NOTHING); - // collections may also be represented without [] so we need to register both paths - if (isCollection(currentPath)) { - addAction(inlineCollectionPath(currentPath), property, DO_NOTHING); - } - } else if (property.isMap()) { - // the enclosure of the map is this group - processLazyMapInGroup(currentPath, matchActions, defaultValues, property.asMap(), getEnclosingFunction, - namingStrategy, group); - } else if (property.isCollection()) { - CollectionProperty collectionProperty = property.asCollection(); - currentPath.addLast(currentPath.removeLast() + "[*]"); - processProperty(currentPath, matchActions, defaultValues, namingStrategy, group, getEnclosingFunction, type, - memberName, collectionProperty.getElement()); + if (roots.isEmpty()) { + return Collections.emptyMap(); } - } - private void processOptionalProperty( - final ArrayDeque currentPath, - final KeyMap> matchActions, - final KeyMap defaultValues, - final NamingStrategy namingStrategy, - final ConfigMappingInterface group, - final BiFunction getEnclosingFunction, - final Class type, final String memberName, final Property property) { - - if (property.isGroup()) { - GroupProperty nestedGroup = property.asGroup(); - // on match, always create the outermost group, which recursively creates inner groups - GetOrCreateEnclosingGroupInGroup matchAction = new GetOrCreateEnclosingGroupInGroup( - getEnclosingFunction, group, nestedGroup, currentPath); - GetFieldOfEnclosing ef = new GetFieldOfEnclosing( - nestedGroup.isParentPropertyName() ? getEnclosingFunction - : new ConsumeOneAndThenFn<>(getEnclosingFunction), - type, memberName); - processLazyGroupInGroup(currentPath, matchActions, defaultValues, namingStrategy, nestedGroup.getGroupType(), ef, - matchAction, - new HashSet<>()); - } else if (property.isLeaf()) { - LeafProperty leafProperty = property.asLeaf(); - if (leafProperty.hasDefaultValue()) { - defaultValues.findOrAdd(currentPath).putRootValue(leafProperty.getDefaultValue()); - // collections may also be represented without [] so we need to register both paths - if (isCollection(currentPath)) { - defaultValues.findOrAdd(inlineCollectionPath(currentPath)).putRootValue(leafProperty.getDefaultValue()); - } + // Perform the config mapping + ConfigMappingContext context = SecretKeys.doUnlocked(new Supplier() { + @Override + public ConfigMappingContext get() { + return new ConfigMappingContext(config, roots, names); } - addAction(currentPath, property, DO_NOTHING); - // collections may also be represented without [] so we need to register both paths - if (isCollection(currentPath)) { - addAction(inlineCollectionPath(currentPath), property, DO_NOTHING); - } - } else if (property.isCollection()) { - CollectionProperty collectionProperty = property.asCollection(); - currentPath.addLast(currentPath.removeLast() + "[*]"); - processProperty(currentPath, matchActions, defaultValues, namingStrategy, group, getEnclosingFunction, type, - memberName, collectionProperty.getElement()); - } - } + }); - private void processLazyGroupInGroup( - final ArrayDeque currentPath, - final KeyMap> matchActions, - final KeyMap defaultValues, - final NamingStrategy namingStrategy, - final ConfigMappingInterface group, - final BiFunction getEnclosingFunction, - final BiConsumer matchAction, - final HashSet usedProperties) { - - int pc = group.getPropertyCount(); - int pathLen = currentPath.size(); - for (int i = 0; i < pc; i++) { - Property property = group.getProperty(i); - if (!property.isParentPropertyName()) { - NameIterator ni = new NameIterator(property.hasPropertyName() ? property.getPropertyName() - : propertyName(property, group, namingStrategy)); - while (ni.hasNext()) { - currentPath.add(ni.getNextSegment()); - ni.next(); - } - } - if (matchActions.hasRootValue(currentPath)) { - while (currentPath.size() > pathLen) { - currentPath.removeLast(); - } - continue; - } - if (usedProperties.add(String.join(".", String.join(".", currentPath), property.getMethod().getName()))) { - boolean optional = property.isOptional(); - processLazyPropertyInGroup(currentPath, matchActions, defaultValues, getEnclosingFunction, matchAction, - usedProperties, namingStrategy, group, optional, property); - } - while (currentPath.size() > pathLen) { - currentPath.removeLast(); - } + if (config.getOptionalValue(SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN, boolean.class).orElse(this.validateUnknown)) { + context.reportUnknown(ignoredPaths); } - int sc = group.getSuperTypeCount(); - for (int i = 0; i < sc; i++) { - processLazyGroupInGroup(currentPath, matchActions, defaultValues, namingStrategy, group.getSuperType(i), - getEnclosingFunction, matchAction, usedProperties); - } - } - private void processLazyPropertyInGroup( - final ArrayDeque currentPath, - final KeyMap> matchActions, - final KeyMap defaultValues, - final BiFunction getEnclosingFunction, - final BiConsumer matchAction, - final HashSet usedProperties, - final NamingStrategy namingStrategy, - final ConfigMappingInterface group, - final boolean optional, - final Property property) { - - if (optional && property.asOptional().getNestedProperty().isGroup()) { - GroupProperty nestedGroup = property.asOptional().getNestedProperty().asGroup(); - GetOrCreateEnclosingGroupInGroup nestedMatchAction = new GetOrCreateEnclosingGroupInGroup( - property.isParentPropertyName() ? new GetNestedEnclosing(matchAction) - : new ConsumeOneAndThenFn<>(new GetNestedEnclosing(matchAction)), - group, nestedGroup, currentPath); - processLazyGroupInGroup(currentPath, matchActions, defaultValues, namingStrategy, nestedGroup.getGroupType(), - nestedMatchAction, nestedMatchAction, new HashSet<>()); - } else if (property.isGroup()) { - GroupProperty asGroup = property.asGroup(); - GetOrCreateEnclosingGroupInGroup nestedEnclosingFunction = new GetOrCreateEnclosingGroupInGroup( - property.isParentPropertyName() ? getEnclosingFunction - : new ConsumeOneAndThenFn<>(getEnclosingFunction), - group, asGroup, currentPath); - BiConsumer nestedMatchAction; - nestedMatchAction = matchAction; - if (!property.isParentPropertyName()) { - nestedMatchAction = new ConsumeOneAndThen(nestedMatchAction); - } - processLazyGroupInGroup(currentPath, matchActions, defaultValues, namingStrategy, asGroup.getGroupType(), - nestedEnclosingFunction, - nestedMatchAction, usedProperties); - } else if (property.isLeaf() || property.isPrimitive() - || optional && property.asOptional().getNestedProperty().isLeaf()) { - BiConsumer actualAction; - if (!property.isParentPropertyName()) { - actualAction = new ConsumeOneAndThen(matchAction); - } else { - actualAction = matchAction; - } - addAction(currentPath, property, actualAction); - // collections may also be represented without [] so we need to register both paths - if (isCollection(currentPath)) { - addAction(inlineCollectionPath(currentPath), property, actualAction); - } - if (property.isPrimitive()) { - PrimitiveProperty primitiveProperty = property.asPrimitive(); - if (primitiveProperty.hasDefaultValue()) { - defaultValues.findOrAdd(currentPath).putRootValue(primitiveProperty.getDefaultValue()); - // collections may also be represented without [] so we need to register both paths - if (isCollection(currentPath)) { - defaultValues.findOrAdd(inlineCollectionPath(currentPath)) - .putRootValue(primitiveProperty.getDefaultValue()); - } - } - } else if (property.isLeaf() && optional) { - LeafProperty leafProperty = property.asOptional().getNestedProperty().asLeaf(); - if (leafProperty.hasDefaultValue()) { - defaultValues.findOrAdd(currentPath).putRootValue(leafProperty.getDefaultValue()); - // collections may also be represented without [] so we need to register both paths - if (isCollection(currentPath)) { - defaultValues.findOrAdd(inlineCollectionPath(currentPath)).putRootValue(leafProperty.getDefaultValue()); - } - } - } else { - LeafProperty leafProperty = property.asLeaf(); - if (leafProperty.hasDefaultValue()) { - defaultValues.findOrAdd(currentPath).putRootValue(leafProperty.getDefaultValue()); - // collections may also be represented without [] so we need to register both paths - if (isCollection(currentPath)) { - defaultValues.findOrAdd(inlineCollectionPath(currentPath)).putRootValue(leafProperty.getDefaultValue()); - } - } - } - } else if (property.isMap()) { - GetNestedEnclosing nestedMatchAction = new GetNestedEnclosing(matchAction); - processLazyMapInGroup(currentPath, matchActions, defaultValues, property.asMap(), nestedMatchAction, namingStrategy, - group); - } else if (property.isCollection() || optional && property.asOptional().getNestedProperty().isCollection()) { - CollectionProperty collectionProperty = optional ? property.asOptional().getNestedProperty().asCollection() - : property.asCollection(); - currentPath.addLast(currentPath.removeLast() + "[*]"); - processLazyPropertyInGroup(currentPath, matchActions, defaultValues, getEnclosingFunction, matchAction, - usedProperties, namingStrategy, group, false, collectionProperty.getElement()); + List problems = context.getProblems(); + if (!problems.isEmpty()) { + throw new ConfigValidationException(problems.toArray(ConfigValidationException.Problem.NO_PROBLEMS)); } - } - private void processLazyMapInGroup( - final ArrayDeque currentPath, - final KeyMap> matchActions, - final KeyMap defaultValues, - final MapProperty property, - final BiFunction getEnclosingGroup, - final NamingStrategy namingStrategy, - final ConfigMappingInterface enclosingGroup) { - - GetOrCreateEnclosingMapInGroup getEnclosingMap = new GetOrCreateEnclosingMapInGroup(getEnclosingGroup, enclosingGroup, - property, currentPath); - processLazyMap(currentPath, matchActions, defaultValues, property, getEnclosingMap, namingStrategy, enclosingGroup); + return context.getRootsMap(); } - private void processLazyMap( - final ArrayDeque currentPath, - final KeyMap> matchActions, - final KeyMap defaultValues, - final MapProperty property, - final BiFunction> getEnclosingMap, - final NamingStrategy namingStrategy, - final ConfigMappingInterface enclosingGroup) { - - Property valueProperty = property.getValueProperty(); - Class> keyConvertWith = property.hasKeyConvertWith() ? property.getKeyConvertWith() : null; - Class keyRawType = property.getKeyRawType(); - currentPath.addLast("*"); - processLazyMapValue(currentPath, matchActions, defaultValues, property, valueProperty, keyConvertWith, keyRawType, - getEnclosingMap, namingStrategy, enclosingGroup); - } + private void matchPropertiesWithEnv(final SmallRyeConfig config) { + // TODO - We shouldn't be mutating the EnvSource. + // We should do the calculation when creating the EnvSource, but right now mappings and sources are not well integrated. - @SuppressWarnings({ "unchecked", "rawtypes" }) - private void processLazyMapValue( - final ArrayDeque currentPath, - final KeyMap> matchActions, - final KeyMap defaultValues, - final MapProperty mapProperty, - final Property valueProperty, - final Class> keyConvertWith, - final Class keyRawType, - final BiFunction> getEnclosingMap, - final NamingStrategy namingStrategy, - final ConfigMappingInterface enclosingGroup) { + String[] profiles = config.getProfiles().toArray(new String[0]); + boolean all = roots.containsKey(""); + StringBuilder sb = new StringBuilder(); - if (valueProperty.isLeaf()) { - if (matchActions.hasRootValue(currentPath)) { - currentPath.removeLast(); - return; + for (ConfigSource configSource : config.getConfigSources(EnvConfigSource.class)) { + if (roots.isEmpty()) { + break; } - LeafProperty leafProperty = valueProperty.asLeaf(); - Class> valConvertWith = leafProperty.getConvertWith(); - Class valueRawType = leafProperty.getValueRawType(); - - addAction(currentPath, mapProperty, (mc, ni) -> { - StringBuilder sb = mc.getStringBuilder(); - // We may need to reset the StringBuilder because the delegate may not be a Map - boolean restore = false; - if (ni.getPosition() != -1) { - restore = true; - ni.previous(); - sb.setLength(0); - sb.append(ni.getAllPreviousSegments()); - } - Map map = getEnclosingMap.apply(mc, ni); - if (restore) { - ni.next(); - sb.setLength(0); - sb.append(ni.getAllPreviousSegments()); - } - - String rawMapKey; - String configKey; - boolean indexed = isIndexed(ni.getPreviousSegment()); - if (indexed && ni.hasPrevious()) { - rawMapKey = normalizeIfIndexed(ni.getPreviousSegment()); - ni.previous(); - configKey = ni.getAllPreviousSegmentsWith(rawMapKey); - ni.next(); - } else { - rawMapKey = ni.getPreviousSegment(); - configKey = ni.getAllPreviousSegments(); - } - - Converter keyConv; - SmallRyeConfig config = mc.getConfig(); - if (keyConvertWith != null) { - keyConv = mc.getConverterInstance(keyConvertWith); - } else { - keyConv = config.requireConverter(keyRawType); - } - Converter valueConv; - if (valConvertWith != null) { - valueConv = mc.getConverterInstance(valConvertWith); - } else { - valueConv = config.requireConverter(valueRawType); - } - - if (indexed) { - CollectionProperty collectionProperty = mapProperty.getValueProperty().asCollection(); - Class collectionRawType = collectionProperty.getCollectionRawType(); - IntFunction collectionFactory = ConfigMappingContext.createCollectionFactory(collectionRawType); - ((Map) map).put(keyConv.convert(rawMapKey), config.getValues(configKey, valueConv, collectionFactory)); + EnvConfigSource envConfigSource = (EnvConfigSource) configSource; + Set mutableEnvProperties = envConfigSource.getPropertyNames(); + List envProperties = new ArrayList<>(mutableEnvProperties); + for (String envProperty : envProperties) { + String activeEnvProperty; + if (envProperty.charAt(0) == '%') { + activeEnvProperty = activeName(envProperty, profiles); } else { - ((Map) map).put(keyConv.convert(rawMapKey), config.getValue(configKey, valueConv)); + activeEnvProperty = envProperty; } - }); - // collections may also be represented without [] so we need to register both paths - if (isCollection(currentPath)) { - addAction(inlineCollectionPath(currentPath), leafProperty, DO_NOTHING); - } - - } else if (valueProperty.isMap()) { - processLazyMap(currentPath, matchActions, defaultValues, valueProperty.asMap(), (mc, ni) -> { - ni.previous(); - Map enclosingMap = getEnclosingMap.apply(mc, ni); - ni.next(); - String rawMapKey = ni.getPreviousSegment(); - Converter keyConv; - SmallRyeConfig config = mc.getConfig(); - if (keyConvertWith != null) { - keyConv = mc.getConverterInstance(keyConvertWith); + String matchedRoot = null; + if (!all) { + for (String root : roots.keySet()) { + if (isEnvPropertyInRoot(activeEnvProperty, root)) { + matchedRoot = root; + break; + } + } + if (matchedRoot == null) { + continue; + } } else { - keyConv = config.requireConverter(keyRawType); - } - Object key = keyConv.convert(rawMapKey); - return (Map) ((Map) enclosingMap).computeIfAbsent(key, x -> new HashMap<>()); - }, namingStrategy, enclosingGroup); - } else if (valueProperty.isGroup()) { - GetOrCreateEnclosingGroupInMap ef = new GetOrCreateEnclosingGroupInMap(getEnclosingMap, mapProperty, enclosingGroup, - valueProperty.asGroup()); - processLazyGroupInGroup(currentPath, matchActions, defaultValues, namingStrategy, - valueProperty.asGroup().getGroupType(), ef, ef, new HashSet<>()); - } else if (valueProperty.isCollection()) { - CollectionProperty collectionProperty = valueProperty.asCollection(); - Property element = collectionProperty.getElement(); - currentPath.addLast(currentPath.removeLast() + "[*]"); - processLazyMapValue(currentPath, matchActions, defaultValues, mapProperty, element, keyConvertWith, keyRawType, - getEnclosingMap, namingStrategy, enclosingGroup); - } else { - throw new UnsupportedOperationException(); - } - } - - private void addAction( - final ArrayDeque currentPath, - final Property property, - final BiConsumer action) { - matchActions.findOrAdd(currentPath).putRootValue(action); - properties.put(String.join(".", currentPath), property); - } - - private static boolean isCollection(final ArrayDeque currentPath) { - return currentPath.getLast().endsWith("[*]"); - } - - private static ArrayDeque inlineCollectionPath(final ArrayDeque currentPath) { - ArrayDeque inlineCollectionPath = new ArrayDeque<>(currentPath); - String last = inlineCollectionPath.removeLast(); - inlineCollectionPath.addLast(last.substring(0, last.length() - 3)); - return inlineCollectionPath; - } - - // This will add the property index (if exists) to the name - private static String indexName(final String name, final String groupPath, final NameIterator nameIterator) { - String group = new NameIterator(groupPath, true).getPreviousSegment(); - String property = nameIterator.getAllPreviousSegments(); - int start = property.lastIndexOf(normalizeIfIndexed(group)); - if (start != -1) { - int i = start + normalizeIfIndexed(group).length(); - if (i < property.length() && property.charAt(i) == '[') { - for (;;) { - if (property.charAt(i) == ']') { - try { - int index = parseInt( - property.substring(start + normalizeIfIndexed(group).length() + 1, i)); - return name + "[" + index + "]"; - } catch (NumberFormatException e) { - //NOOP + matchedRoot = ""; + } + + for (Map> rootNames : names.values()) { + Set mappedProperties = rootNames.get(matchedRoot); + if (mappedProperties != null) { + for (String mappedProperty : mappedProperties) { + // Try to match Env with Root mapped property and generate the expected format + List indexOfDashes = indexOfDashes(mappedProperty, activeEnvProperty); + if (indexOfDashes != null) { + sb.append(activeEnvProperty); + for (Integer dash : indexOfDashes) { + sb.setCharAt(dash, '-'); + } + String expectedEnvProperty = sb.toString(); + if (!activeEnvProperty.equals(expectedEnvProperty)) { + envConfigSource.getPropertyNames().add(sb.toString()); + envConfigSource.getPropertyNames().remove(envProperty); + ignoredPaths.add(activeEnvProperty); + } + sb.setLength(0); + break; + } } break; - } else if (i < property.length() - 1) { - i++; - } else { - break; } } } } - return name; - } - - private static String propertyName(final Property property, final ConfigMappingInterface group, - final NamingStrategy namingStrategy) { - return namingStrategy(namingStrategy, group.getNamingStrategy()).apply(property.getPropertyName()); - } - - private static NamingStrategy namingStrategy(NamingStrategy parent, NamingStrategy current) { - if (!current.isDefault()) { - return current; - } else { - return parent; - } - } - - static class GetRootAction implements BiFunction { - private final Class root; - private final String rootPath; - - GetRootAction(final Class root, final String rootPath) { - this.root = root; - this.rootPath = rootPath; - } - - @Override - public ConfigMappingObject apply(final ConfigMappingContext mc, final NameIterator ni) { - return mc.getRoot(root, rootPath); - } - } - - static class GetOrCreateEnclosingGroupInGroup - implements BiFunction, - BiConsumer { - private final BiFunction delegate; - private final ConfigMappingInterface enclosingGroup; - private final GroupProperty enclosedGroup; - private final String groupPath; - - GetOrCreateEnclosingGroupInGroup( - final BiFunction delegate, - final ConfigMappingInterface enclosingGroup, - final GroupProperty enclosedGroup, - final ArrayDeque path) { - this.delegate = delegate; - this.enclosingGroup = enclosingGroup; - this.enclosedGroup = enclosedGroup; - this.groupPath = String.join(".", path); - } - - @Override - public ConfigMappingObject apply(final ConfigMappingContext context, final NameIterator ni) { - ConfigMappingObject ourEnclosing = delegate.apply(context, ni); - Class enclosingType = enclosingGroup.getInterfaceType(); - String key = indexName(enclosedGroup.getMethod().getName(), groupPath, ni); - ConfigMappingObject val = (ConfigMappingObject) context.getEnclosedField(enclosingType, key, ourEnclosing); - context.applyNamingStrategy( - namingStrategy(enclosedGroup.getGroupType().getNamingStrategy(), enclosingGroup.getNamingStrategy())); - if (val == null) { - // it must be an optional group - StringBuilder sb = context.getStringBuilder(); - sb.replace(0, sb.length(), ni.getAllPreviousSegments()); - val = (ConfigMappingObject) context.constructGroup(enclosedGroup.getGroupType().getInterfaceType()); - context.registerEnclosedField(enclosingType, key, ourEnclosing, val); - } - return val; - } - - @Override - public void accept(final ConfigMappingContext context, final NameIterator nameIterator) { - apply(context, nameIterator); - } - } - - static class GetOrCreateEnclosingGroupInMap implements BiFunction, - BiConsumer { - private final BiFunction> getEnclosingMap; - private final MapProperty enclosingMap; - private final ConfigMappingInterface enclosingGroup; - private final GroupProperty enclosedGroup; - - GetOrCreateEnclosingGroupInMap( - final BiFunction> getEnclosingMap, - final MapProperty enclosingMap, - final ConfigMappingInterface enclosingGroup, - final GroupProperty enclosedGroup) { - this.getEnclosingMap = getEnclosingMap; - this.enclosingMap = enclosingMap; - this.enclosingGroup = enclosingGroup; - this.enclosedGroup = enclosedGroup; - } - - @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - public ConfigMappingObject apply(final ConfigMappingContext context, final NameIterator ni) { - ni.previous(); - Map ourEnclosing = getEnclosingMap.apply(context, ni); - ni.next(); - Converter keyConverter = context.getKeyConverter(enclosingGroup.getInterfaceType(), - enclosingMap.getMethod().getName(), enclosingMap.getLevels() - 1); - MapKey mapKey; - if (enclosingMap.getValueProperty().isCollection()) { - mapKey = MapKey.collectionKey(ni.getPreviousSegment(), keyConverter); - } else { - mapKey = MapKey.key(ni.getPreviousSegment(), ni.getAllPreviousSegments(), keyConverter); - } - ConfigMappingObject val = (ConfigMappingObject) context.getEnclosedField(enclosingGroup.getInterfaceType(), - mapKey.getKey(), - ourEnclosing); - if (val == null) { - StringBuilder sb = context.getStringBuilder(); - - ni.previous(); - sb.replace(0, sb.length(), ni.getAllPreviousSegmentsWith(mapKey.getKey())); - ni.next(); - - context.applyNamingStrategy( - namingStrategy(enclosedGroup.getGroupType().getNamingStrategy(), enclosingGroup.getNamingStrategy())); - val = (ConfigMappingObject) context.constructGroup(enclosedGroup.getGroupType().getInterfaceType()); - context.registerEnclosedField(enclosingGroup.getInterfaceType(), mapKey.getKey(), ourEnclosing, val); - - if (enclosingMap.getValueProperty().isCollection()) { - CollectionProperty collectionProperty = enclosingMap.getValueProperty().asCollection(); - Collection collection = (Collection) ourEnclosing.get(mapKey.getConvertedKey()); - if (collection == null) { - // Create the Collection in the Map does not have it - Class collectionRawType = collectionProperty.getCollectionRawType(); - IntFunction> collectionFactory = ConfigMappingContext - .createCollectionFactory(collectionRawType); - ni.previous(); - // Get all the available indexes - List indexes = context.getConfig().getIndexedPropertiesIndexes( - ni.getAllPreviousSegmentsWith(normalizeIfIndexed(mapKey.getKey()))); - ni.next(); - collection = collectionFactory.apply(indexes.size()); - // Initialize all expected elements in the list - if (collection instanceof List) { - for (Integer index : indexes) { - ((List) collection).add(index, null); - } + // Match dotted properties from other sources with Env with the same semantic meaning + // This needs to happen after matching dashed names from mappings + List dottedProperties = new ArrayList<>(); + for (ConfigSource configSource : config.getConfigSources()) { + if (!(configSource instanceof EnvConfigSource)) { + Set propertyNames = configSource.getPropertyNames(); + if (propertyNames != null) { + dottedProperties.addAll(propertyNames.stream().map(new Function() { + @Override + public String apply(final String name) { + return activeName(name, profiles); } - ((Map) ourEnclosing).put(mapKey.getConvertedKey(), collection); - } - - if (collection instanceof List) { - // We don't know the order in which the properties will be processed, so we set it manually - ((List) collection).set(mapKey.getIndex(), val); - } else { - collection.add(val); - } - } else { - ((Map) ourEnclosing).put(mapKey.getConvertedKey(), val); + }).collect(Collectors.toList())); } } - return val; } - @Override - public void accept(final ConfigMappingContext context, final NameIterator ni) { - apply(context, ni); - } - - static class MapKey { - private final String key; - private final Object convertedKey; - - public MapKey(final String key, final Object convertedKey) { - this.key = key; - this.convertedKey = convertedKey; - } - - public String getKey() { - return key; - } - - public Object getConvertedKey() { - return convertedKey; - } - - public int getIndex() { - int indexStart = key.indexOf("["); - int indexEnd = key.indexOf("]"); - if (indexStart != -1 && indexEnd != -1) { - String index = key.substring(indexStart + 1, indexEnd); - try { - return Integer.parseInt(index); - } catch (NumberFormatException e) { - throw new IllegalArgumentException(); + for (ConfigSource configSource : config.getConfigSources(EnvConfigSource.class)) { + EnvConfigSource envConfigSource = (EnvConfigSource) configSource; + for (String dottedProperty : dottedProperties) { + Set envNames = envConfigSource.getPropertyNames(); + if (envConfigSource.hasPropertyName(dottedProperty)) { + if (!envNames.contains(dottedProperty)) { + // this may be expensive, but it shouldn't happend that often + envNames.remove(toLowerCaseAndDotted(replaceNonAlphanumericByUnderscores(dottedProperty))); + envNames.add(dottedProperty); } } - throw new IllegalArgumentException(); - } - - // NameIterator#getPreviousSegment() returns the name without quotes. - // We need add them if they exist, so we cannot reuse mapKey, or we are going to get NoSuchElement - static MapKey key(final String mapKey, final String mapPath, final Converter keyConverter) { - String key = mapKey; - if (mapPath.charAt(mapPath.length() - 1) == '"' - && mapPath.charAt(mapPath.length() - 1 - mapKey.length() - 1) == '"') { - key = "\"" + mapKey + "\""; - } - // Remove indexes to construct the group. The group constructor handles the indexes - - Object convertedKey = keyConverter.convert(mapKey); - if (convertedKey.equals(mapKey)) { - convertedKey = normalizeIfIndexed(mapKey); - } - - return new MapKey(normalizeIfIndexed(key), convertedKey); - } - - static MapKey collectionKey(final String mapKey, final Converter keyConverter) { - Object convertedKey = keyConverter.convert(mapKey); - if (convertedKey.equals(mapKey)) { - convertedKey = normalizeIfIndexed(mapKey); - } - - return new MapKey(mapKey, convertedKey); - } - } - } - - static class GetOrCreateEnclosingMapInGroup implements BiFunction>, - BiConsumer { - private final BiFunction delegate; - private final ConfigMappingInterface enclosingGroup; - private final MapProperty enclosedGroup; - private final String groupPath; - - GetOrCreateEnclosingMapInGroup( - final BiFunction delegate, - final ConfigMappingInterface enclosingGroup, - final MapProperty enclosedGroup, - final ArrayDeque path) { - this.delegate = delegate; - this.enclosingGroup = enclosingGroup; - this.enclosedGroup = enclosedGroup; - this.groupPath = String.join(".", path); - } - - @Override - public Map apply(final ConfigMappingContext context, final NameIterator ni) { - boolean consumeName = !enclosedGroup.isParentPropertyName(); - if (consumeName) - ni.previous(); - ConfigMappingObject ourEnclosing = delegate.apply(context, ni); - if (consumeName) - ni.next(); - Class enclosingType = enclosingGroup.getInterfaceType(); - String key = indexName(enclosedGroup.getMethod().getName(), groupPath, ni); - Map val = (Map) context.getEnclosedField(enclosingType, key, ourEnclosing); - context.applyNamingStrategy(enclosingGroup.getNamingStrategy()); - if (val == null) { - // map is not yet constructed - val = new HashMap<>(); - context.registerEnclosedField(enclosingType, key, ourEnclosing, val); } - return val; - } - - @Override - public void accept(final ConfigMappingContext context, final NameIterator ni) { - apply(context, ni); - } - } - - static class GetFieldOfEnclosing implements BiFunction { - private final BiFunction getEnclosingFunction; - private final Class type; - private final String memberName; - - GetFieldOfEnclosing(final BiFunction getEnclosingFunction, - final Class type, final String memberName) { - this.getEnclosingFunction = getEnclosingFunction; - this.type = type; - this.memberName = memberName; - } - - @Override - public ConfigMappingObject apply(final ConfigMappingContext context, final NameIterator ni) { - ConfigMappingObject outer = getEnclosingFunction.apply(context, ni); - // eagerly populated groups will always exist - return (ConfigMappingObject) context.getEnclosedField(type, memberName, outer); } } - // To recursively create Optional nested groups - static class GetNestedEnclosing implements BiFunction { - private final BiConsumer matchAction; - - public GetNestedEnclosing(final BiConsumer matchAction) { - this.matchAction = matchAction; - } - - @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - public ConfigMappingObject apply(final ConfigMappingContext configMappingContext, final NameIterator nameIterator) { - if (matchAction instanceof BiFunction) { - return (ConfigMappingObject) ((BiFunction) matchAction).apply(configMappingContext, nameIterator); - } - return null; - } - } - - public static Builder builder() { - return new Builder(); - } - - KeyMap> getMatchActions() { - return matchActions; - } - - Map getProperties() { - return properties; - } - - KeyMap getDefaultValues() { - return defaultValues; - } - - void mapConfiguration(SmallRyeConfig config) throws ConfigValidationException { - for (ConfigSource configSource : config.getConfigSources()) { - if (configSource instanceof DefaultValuesConfigSource) { - final DefaultValuesConfigSource defaultValuesConfigSource = (DefaultValuesConfigSource) configSource; - defaultValuesConfigSource.registerDefaults(this.getDefaultValues()); - } - } - - config.addPropertyNames(additionalMappedProperties(new HashSet<>(getProperties().keySet()), config)); - SecretKeys.doUnlocked(() -> mapConfiguration(config, config.getConfigMappings())); - } - - private void mapConfiguration(SmallRyeConfig config, ConfigMappings mappings) throws ConfigValidationException { - if (roots.isEmpty()) { - return; - } - - Assert.checkNotNullParam("config", config); - ConfigMappingContext context = new ConfigMappingContext(config); - // eagerly populate roots - for (Map.Entry>> entry : roots.entrySet()) { - String path = entry.getKey(); - List> roots = entry.getValue(); - for (Class root : roots) { - StringBuilder sb = context.getStringBuilder(); - sb.replace(0, sb.length(), path); - ConfigMappingObject group = (ConfigMappingObject) context.constructRoot(root); - context.registerRoot(root, path, group); - } - } - - // lazily sweep - Set unknownProperties = new HashSet<>(); - for (String name : config.getPropertyNames()) { - NameIterator ni = new NameIterator(name); - // filter properties in root - if (!isPropertyInRoot(ni)) { - continue; - } - - BiConsumer action = matchActions.findRootValue(ni); - if (action != null) { - action.accept(context, ni); - } else { - if (validateUnknown(validateUnknown, config)) { - unknownProperties.add(name); - } - } - } - - unknownProperties(unknownProperties, context); - ArrayList problems = context.getProblems(); - if (!problems.isEmpty()) { - throw new ConfigValidationException(problems.toArray(ConfigValidationException.Problem.NO_PROBLEMS)); - } - context.fillInOptionals(); - - mappings.registerConfigMappings(context.getRootsMap()); - } - - private boolean isPropertyInRoot(NameIterator propertyName) { - final Set registeredRoots = roots.keySet(); - for (String registeredRoot : registeredRoots) { - // match everything - if (registeredRoot.length() == 0) { - return true; - } - - // A sub property from a namespace is always bigger in length - if (propertyName.getName().length() <= registeredRoot.length()) { - continue; - } - - final NameIterator root = new NameIterator(registeredRoot); - // compare segments - while (root.hasNext()) { - String segment = root.getNextSegment(); - if (!propertyName.hasNext()) { - propertyName.goToStart(); - break; - } - - final String nextSegment = propertyName.getNextSegment(); - if (!segment.equals(normalizeIfIndexed(nextSegment))) { - propertyName.goToStart(); - break; - } - - root.next(); - propertyName.next(); - - // root has no more segments and we reached this far so everything matched. - // on top, property still has more segments to do the mapping. - if (!root.hasNext() && propertyName.hasNext()) { - propertyName.goToStart(); - return true; + /** + * Matches if a dotted Environment property name is part of a registered root. + * + * @param envProperty a generated dotted property from the {@link EnvConfigSource}. + * @param root the root name + * @return true if the env property ir part of the root, or false otherwise. + */ + private static boolean isEnvPropertyInRoot(final String envProperty, final String root) { + if (envProperty.equals(root)) { + return true; + } + + // if property is less than the root no way to match + if (envProperty.length() <= root.length()) { + return false; + } + + // foo.bar + // foo.bar."baz" + // foo.bar[0] + char e = envProperty.charAt(root.length()); + if ((e == '.') || e == '[') { + for (int i = 0; i < root.length(); i++) { + char r = root.charAt(i); + e = envProperty.charAt(i); + if (r == '-') { + if (e != '.' && e != '-') { + return false; + } + } else if (r != e) { + return false; } } + return true; } - return false; } - private static Set additionalMappedProperties(final Set mappedProperties, final SmallRyeConfig config) { - // Collect EnvSource properties - Set envProperties = new HashSet<>(); - for (ConfigSource source : config.getConfigSources(EnvConfigSource.class)) { - envProperties.addAll(source.getPropertyNames()); + /** + * Finds and returns all indexes from a dotted Environment property name, related to its matched mapped + * property name that must be replaced with a dash. This allows to set single environment variables as + * FOO_BAR_BAZ and match them to mappeds properties like foo.*.baz, + * foo-bar.baz or any other combinations find in mappings, without the need of additional metadata. + * + * @param mappedProperty the mapping property name. + * @param envProperty a generated dotted property from the {@link EnvConfigSource}. + * @return a List of indexes from the env property name to replace with a dash. + */ + private static List indexOfDashes(final String mappedProperty, final String envProperty) { + if (mappedProperty.length() > envProperty.length()) { + return null; } - // Remove properties that we know that already exist - for (String propertyName : config.getPropertyNames()) { - mappedProperties.remove(propertyName); - } + List dashesPosition = null; + int matchPosition = envProperty.length() - 1; + for (int i = mappedProperty.length() - 1; i >= 0; i--) { + if (matchPosition == -1) { + return null; + } - Set additionalMappedProperties = new HashSet<>(); - // Look for unmatched properties if we can find one in the Env ones and add it - for (String mappedProperty : mappedProperties) { - Set matchedEnvProperties = new HashSet<>(); - for (String envProperty : envProperties) { - if (envProperty.equalsIgnoreCase(replaceNonAlphanumericByUnderscores(mappedProperty))) { - additionalMappedProperties.add(mappedProperty); - matchedEnvProperties.add(envProperty); - break; + char c = mappedProperty.charAt(i); + if (c == '.' || c == '-') { + char p = envProperty.charAt(matchPosition); + if (p != '.' && p != '-') { // a property coming from env can either be . or - + return null; } - - NameIterator ni = new NameIterator(mappedProperty); - StringBuilder sb = new StringBuilder(); - while (ni.hasNext()) { - String propertySegment = ni.getNextSegment(); - if (isIndexed(propertySegment)) { - // A mapped index property is represented as foo.bar[*] or foo.bar[*].baz - // The env property is represented as FOO_BAR_0_ or FOO_BAR_0__BAZ - // We need to match these somehow - int position = ni.getPosition(); - int indexStart = propertySegment.indexOf("[") + position + 1; - // If the segment is indexed, we try to match all previous segments with the env candidates - if (envProperty.length() >= indexStart - && envProperty.toLowerCase().startsWith(replaceNonAlphanumericByUnderscores( - sb + propertySegment.substring(0, indexStart - position - 1) + "_"))) { - - // Search for the ending _ to retrieve the possible index - int indexEnd = -1; - for (int i = indexStart + 1; i < envProperty.length(); i++) { - if (envProperty.charAt(i) == '_') { - indexEnd = i; - break; - } - } - - // Extract the index from the env property - // We don't care if this is numeric, it will be validated on the mapping retrieval - String index = envProperty.substring(indexStart + 1, indexEnd); - sb.append(propertySegment, 0, propertySegment.indexOf("[") + 1) - .append(index) - .append("]"); - } - } else { - sb.append(propertySegment); + if (c == '-') { + if (dashesPosition == null) { + dashesPosition = new ArrayList<>(); } - - ni.next(); - - if (ni.hasNext()) { - sb.append("."); + dashesPosition.add(matchPosition); + } + matchPosition--; + } else if (c == '*') { // it's a map - skip to next separator + char p = envProperty.charAt(matchPosition); + if (p == '"') { + matchPosition = envProperty.lastIndexOf('"', matchPosition - 1); + if (matchPosition != -1) { + matchPosition = envProperty.lastIndexOf('.', matchPosition); } } - - String mappedPropertyToMatch = sb.toString(); - if (envProperty.equalsIgnoreCase(replaceNonAlphanumericByUnderscores(mappedPropertyToMatch))) { - additionalMappedProperties.add(mappedPropertyToMatch); - matchedEnvProperties.add(envProperty); - // We cannot break here because if there are indexed properties they may match multiple envs + matchPosition = envProperty.lastIndexOf('.', matchPosition); + } else if (c == ']') { // it's a collection - skip to next separator + i = i - 2; + matchPosition = envProperty.lastIndexOf('[', matchPosition); + if (matchPosition != -1) { + matchPosition--; } + } else if (c != envProperty.charAt(matchPosition)) { + return null; + } else { + matchPosition--; } - envProperties.removeAll(matchedEnvProperties); } - - return additionalMappedProperties; + return dashesPosition; } - private static String normalizeIfIndexed(final String propertyName) { - int indexStart = propertyName.indexOf("["); - int indexEnd = propertyName.indexOf("]"); - if (indexStart != -1 && indexEnd != -1) { - String index = propertyName.substring(indexStart + 1, indexEnd); - if (index.equals("*")) { - return propertyName.substring(0, indexStart); - } - try { - Integer.parseInt(index); - return propertyName.substring(0, indexStart); - } catch (NumberFormatException e) { - return propertyName; - } - } - return propertyName; - } + static final class Builder { + Set> types = new HashSet<>(); + Map>> roots = new HashMap<>(); + Set keys = new HashSet<>(); + Map>> names = new HashMap<>(); + List ignoredPaths = new ArrayList<>(); + boolean validateUnknown = true; + SmallRyeConfigBuilder configBuilder = null; - private static boolean isIndexed(final String propertyName) { - int indexStart = propertyName.indexOf("["); - int indexEnd = propertyName.indexOf("]"); - if (indexStart != -1 && indexEnd != -1) { - String index = propertyName.substring(indexStart + 1, indexEnd); - if (index.equals("*")) { - return true; - } - try { - Integer.parseInt(index); - return true; - } catch (NumberFormatException e) { - return false; - } + Builder() { } - return false; - } - - private static boolean validateUnknown(final boolean validateUnknown, final SmallRyeConfig config) { - return config.getOptionalValue(SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN, Boolean.class) - .orElse(validateUnknown); - } - - private static void unknownProperties(Set properties, ConfigMappingContext context) { - Set usedProperties = new HashSet<>(); - for (String property : context.getConfig().getPropertyNames()) { - if (properties.contains(property)) { - continue; - } - usedProperties.add(replaceNonAlphanumericByUnderscores(property)); + Builder addRoot(String prefix, Class type) { + Assert.checkNotNullParam("path", prefix); + Assert.checkNotNullParam("type", type); + types.add(type); + roots.computeIfAbsent(prefix, k -> new ArrayList<>(4)).add(getConfigMappingClass(type)); + return this; } - usedProperties.removeAll(properties); - for (String property : properties) { - boolean found = false; - for (String usedProperty : usedProperties) { - if (usedProperty.equalsIgnoreCase(replaceNonAlphanumericByUnderscores(property))) { - found = true; - break; - } - } - if (!found) { - context.unknownConfigElement(property); - } + Builder keys(Set keys) { + Assert.checkNotNullParam("keys", keys); + this.keys.addAll(keys); + return this; } - } - public static final class Builder { - final Set> types = new HashSet<>(); - final Map>> roots = new HashMap<>(); - final List ignored = new ArrayList<>(); - boolean validateUnknown = true; - - Builder() { + Builder names(Map>> names) { + Assert.checkNotNullParam("names", names); + for (Map.Entry>> entry : names.entrySet()) { + Map> groupNames = this.names.computeIfAbsent(entry.getKey(), + new Function>>() { + @Override + public Map> apply( + final String s) { + return new HashMap<>(); + } + }); + groupNames.putAll(entry.getValue()); + } + return this; } - public Builder addRoot(String path, Class type) { - Assert.checkNotNullParam("path", path); - Assert.checkNotNullParam("type", type); - types.add(type); - roots.computeIfAbsent(path, k -> new ArrayList<>(4)).add(getConfigMappingClass(type)); + Builder ignoredPath(String ignoredPath) { + Assert.checkNotNullParam("ignoredPath", ignoredPath); + ignoredPaths.add(ignoredPath); return this; } - public Builder addIgnored(String path) { - Assert.checkNotNullParam("path", path); - ignored.add(path.split("\\.")); + Builder validateUnknown(boolean validateUnknown) { + this.validateUnknown = validateUnknown; return this; } - public Builder validateUnknown(boolean validateUnknown) { - this.validateUnknown = validateUnknown; + Builder registerDefaults(SmallRyeConfigBuilder configBuilder) { + this.configBuilder = configBuilder; return this; } - public ConfigMappingProvider build() { + ConfigMappingProvider build() { // We don't validate for MP ConfigProperties, so if all classes are MP ConfigProperties disable validation. boolean allConfigurationProperties = true; for (Class type : types) { @@ -1197,7 +342,37 @@ public ConfigMappingProvider build() { } if (allConfigurationProperties) { - this.validateUnknown = false; + validateUnknown = false; + } + + if (keys.isEmpty()) { + for (Map.Entry>> entry : roots.entrySet()) { + for (Class root : entry.getValue()) { + ConfigClassWithPrefix configClass = configClassWithPrefix(root, entry.getKey()); + keys(getProperties(configClass).get(configClass.getKlass()).get(configClass.getPrefix()).keySet()); + } + } + } + + if (names.isEmpty()) { + for (Map.Entry>> entry : roots.entrySet()) { + for (Class root : entry.getValue()) { + ConfigClassWithPrefix configClass = configClassWithPrefix(root, entry.getKey()); + names(getNames(configClass)); + } + } + } + + if (configBuilder != null) { + Map defaultValues = configBuilder.getDefaultValues(); + for (Map.Entry>> entry : roots.entrySet()) { + for (Class root : entry.getValue()) { + for (Map.Entry defaultEntry : getDefaults(configClassWithPrefix(root, entry.getKey())) + .entrySet()) { + defaultValues.putIfAbsent(defaultEntry.getKey(), defaultEntry.getValue()); + } + } + } } return new ConfigMappingProvider(this); diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappings.java b/implementation/src/main/java/io/smallrye/config/ConfigMappings.java index 0fee5d898..6b4056d7a 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappings.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappings.java @@ -1,118 +1,258 @@ package io.smallrye.config; import static io.smallrye.config.ConfigMappingLoader.getConfigMappingClass; +import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN; import static java.lang.Boolean.TRUE; import java.io.Serializable; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; -import java.util.Optional; +import java.util.Objects; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import org.eclipse.microprofile.config.inject.ConfigProperties; +import java.util.function.Function; +import io.smallrye.config.ConfigMapping.NamingStrategy; +import io.smallrye.config.ConfigMappingInterface.CollectionProperty; +import io.smallrye.config.ConfigMappingInterface.GroupProperty; +import io.smallrye.config.ConfigMappingInterface.LeafProperty; import io.smallrye.config.ConfigMappingInterface.Property; public final class ConfigMappings implements Serializable { private static final long serialVersionUID = -7790784345796818526L; - private final ConfigValidator configValidator; - private final ConcurrentMap, Map> mappings; - - ConfigMappings(final ConfigValidator configValidator) { - this.configValidator = configValidator; - this.mappings = new ConcurrentHashMap<>(); - } - - void registerConfigMappings(final Map, Map> mappings) { - this.mappings.putAll(mappings); - } - public static void registerConfigMappings(final SmallRyeConfig config, final Set configClasses) throws ConfigValidationException { if (!configClasses.isEmpty()) { Boolean validateUnknown = config.getOptionalValue(SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN, Boolean.class) .orElse(TRUE); - mapConfiguration(ConfigMappingProvider.builder().validateUnknown(validateUnknown), config, configClasses); + mapConfiguration(config, ConfigMappingProvider.builder().validateUnknown(validateUnknown), configClasses); } } public static void registerConfigProperties(final SmallRyeConfig config, final Set configClasses) throws ConfigValidationException { if (!configClasses.isEmpty()) { - mapConfiguration(ConfigMappingProvider.builder().validateUnknown(false), config, configClasses); + mapConfiguration(config, ConfigMappingProvider.builder().validateUnknown(false), configClasses); } } - public static Map getProperties(final ConfigClassWithPrefix configClass) { - ConfigMappingProvider provider = ConfigMappingProvider.builder() - .validateUnknown(false) - .addRoot(configClass.getPrefix(), configClass.getKlass()) - .build(); + public static Map, Map>> getProperties(final ConfigClassWithPrefix configClass) { + Map, Map>> properties = new HashMap<>(); + Function path = new Function<>() { + @Override + public String apply(final String path) { + return configClass.getPrefix().isEmpty() && !path.isEmpty() ? path.substring(1) + : configClass.getPrefix() + path; + } + }; + ConfigMappingInterface configMapping = ConfigMappingLoader.getConfigMapping(configClass.getKlass()); + getProperties(new GroupProperty(null, null, configMapping), configMapping.getNamingStrategy(), path, properties); + return properties; + } - return provider.getProperties(); + public static Map>> getNames(final ConfigClassWithPrefix configClass) { + Map>> names = new HashMap<>(); + Map, Map>> properties = getProperties(configClass); + for (Map.Entry, Map>> entry : properties.entrySet()) { + Map> groups = new HashMap<>(); + for (Map.Entry> group : entry.getValue().entrySet()) { + groups.put(group.getKey(), group.getValue().keySet()); + } + names.put(entry.getKey().getName(), groups); + } + return names; } - public static Set mappedProperties(final ConfigClassWithPrefix configClass, final Set properties) { - ConfigMappingProvider provider = ConfigMappingProvider.builder() - .validateUnknown(false) - .addRoot(configClass.getPrefix(), configClass.getKlass()) - .build(); + public static Set getKeys(final ConfigClassWithPrefix configClass) { + return getProperties(configClass).get(configClass.getKlass()).get(configClass.getPrefix()).keySet(); + } + + public static Map getDefaults(final ConfigClassWithPrefix configClass) { + Map defaults = new HashMap<>(); + Map, Map>> properties = getProperties(configClass); + for (Map.Entry entry : properties.get(configClass.getKlass()).get(configClass.getPrefix()) + .entrySet()) { + if (entry.getValue().hasDefaultValue()) { + defaults.put(entry.getKey(), entry.getValue().getDefaultValue()); + } + } + return defaults; + } + public static Set mappedProperties(final ConfigClassWithPrefix configClass, final Set properties) { + Set names = new ConfigMappingNames(getNames(configClass)) + .get(configClass.getKlass().getName(), configClass.getPrefix()); Set mappedProperties = new HashSet<>(); for (String property : properties) { - if (provider.getMatchActions().findRootValue(new NameIterator(property)) != null) { + if (names.contains(new PropertyName(property))) { mappedProperties.add(property); } } return mappedProperties; } - static void mapConfiguration( - final ConfigMappingProvider.Builder builder, + private static void mapConfiguration( final SmallRyeConfig config, - final Set configClasses) throws ConfigValidationException { + final ConfigMappingProvider.Builder builder, + final Set configClasses) + throws ConfigValidationException { + DefaultValuesConfigSource defaultValues = (DefaultValuesConfigSource) config.getDefaultValues(); for (ConfigClassWithPrefix configClass : configClasses) { builder.addRoot(configClass.getPrefix(), configClass.getKlass()); + defaultValues.addDefaults( + getDefaults(configClassWithPrefix(getConfigMappingClass(configClass.getKlass()), configClass.getPrefix()))); } - builder.build().mapConfiguration(config); + config.getMappings().putAll(builder.build().mapConfiguration(config)); } - T getConfigMapping(Class type) { - final String prefix = Optional.ofNullable(type.getAnnotation(ConfigMapping.class)) - .map(ConfigMapping::prefix) - .orElseGet(() -> Optional.ofNullable(type.getAnnotation(ConfigProperties.class)).map(ConfigProperties::prefix) - .orElse("")); + private static void getProperties( + final GroupProperty groupProperty, + final NamingStrategy namingStrategy, + final Function path, + final Map, Map>> properties) { - return getConfigMapping(type, prefix); + ConfigMappingInterface groupType = groupProperty.getGroupType(); + Map groupProperties = properties + .computeIfAbsent(groupType.getInterfaceType(), group -> new HashMap<>()) + .computeIfAbsent(path.apply(""), s -> new HashMap<>()); + + getProperties(groupProperty, namingStrategy, path, properties, groupProperties); } - T getConfigMapping(Class type, String prefix) { - if (prefix == null) { - return getConfigMapping(type); - } + private static void getProperties( + final GroupProperty groupProperty, + final NamingStrategy namingStrategy, + final Function path, + final Map, Map>> properties, + final Map groupProperties) { - final Map mappingsForType = mappings.get(getConfigMappingClass(type)); - if (mappingsForType == null) { - throw ConfigMessages.msg.mappingNotFound(type.getName()); + for (Property property : groupProperty.getGroupType().getProperties(true)) { + getProperty(property, namingStrategy, path, properties, groupProperties); } + } - final ConfigMappingObject configMappingObject = mappingsForType.get(prefix); - if (configMappingObject == null) { - throw ConfigMessages.msg.mappingPrefixNotFound(type.getName(), prefix); - } + private static void getProperty( + final Property property, + final NamingStrategy namingStrategy, + final Function path, + final Map, Map>> properties, + final Map groupProperties) { - Object value = configMappingObject; - if (configMappingObject instanceof ConfigMappingClassMapper) { - value = ((ConfigMappingClassMapper) configMappingObject).map(); + if (property.isLeaf()) { + groupProperties.put( + path.apply(property.isParentPropertyName() ? "" : "." + property.getPropertyName(namingStrategy)), + property); + } else if (property.isPrimitive()) { + groupProperties.put( + path.apply(property.isParentPropertyName() ? "" : "." + property.getPropertyName(namingStrategy)), + property); + } else if (property.isGroup()) { + GroupProperty groupProperty = property.asGroup(); + NamingStrategy groupNamingStrategy = groupProperty.hasNamingStrategy() ? groupProperty.getNamingStrategy() + : namingStrategy; + Function groupPath = new Function<>() { + @Override + public String apply(final String name) { + return property.isParentPropertyName() ? path.apply("") + name + : path.apply("." + property.getPropertyName(namingStrategy)) + name; + } + }; + getProperties(groupProperty, groupNamingStrategy, groupPath, properties); + getProperties(groupProperty, groupNamingStrategy, groupPath, properties, groupProperties); + } else if (property.isMap()) { + ConfigMappingInterface.MapProperty mapProperty = property.asMap(); + if (mapProperty.getValueProperty().isLeaf()) { + groupProperties.put(property.isParentPropertyName() ? path.apply(".*") + : path.apply("." + property.getPropertyName(namingStrategy) + ".*"), mapProperty); + if (mapProperty.hasKeyUnnamed()) { + groupProperties.put(property.isParentPropertyName() ? path.apply("") + : path.apply("." + property.getPropertyName(namingStrategy)), mapProperty); + } + } else if (mapProperty.getValueProperty().isGroup()) { + GroupProperty groupProperty = mapProperty.getValueProperty().asGroup(); + NamingStrategy groupNamingStrategy = groupProperty.hasNamingStrategy() ? groupProperty.getNamingStrategy() + : namingStrategy; + Function groupPath = new Function<>() { + @Override + public String apply(final String name) { + return property.isParentPropertyName() ? path.apply(".*") + name + : path.apply("." + mapProperty.getPropertyName(namingStrategy) + ".*") + name; + } + }; + getProperties(groupProperty, groupNamingStrategy, groupPath, properties); + getProperties(groupProperty, groupNamingStrategy, groupPath, properties, groupProperties); + if (mapProperty.hasKeyUnnamed()) { + Function unnamedGroupPath = new Function<>() { + @Override + public String apply(final String name) { + return property.isParentPropertyName() ? path.apply(name) + : path.apply("." + mapProperty.getPropertyName(namingStrategy)) + name; + } + }; + getProperties(groupProperty, groupNamingStrategy, unnamedGroupPath, properties); + getProperties(groupProperty, groupNamingStrategy, unnamedGroupPath, properties, groupProperties); + } + } else if (mapProperty.getValueProperty().isCollection()) { + CollectionProperty collectionProperty = mapProperty.getValueProperty().asCollection(); + Property element = collectionProperty.getElement(); + if (element.isLeaf()) { + LeafProperty leafProperty = new LeafProperty(element.getMethod(), element.getPropertyName(), + element.asLeaf().getValueType(), element.asLeaf().getConvertWith(), null); + getProperty(leafProperty, namingStrategy, new Function() { + @Override + public String apply(final String name) { + return path.apply(name + ".*"); + } + }, properties, groupProperties); + } + getProperty(element, namingStrategy, new Function() { + @Override + public String apply(final String name) { + return path.apply(name + ".*[*]"); + } + }, properties, groupProperties); + if (mapProperty.hasKeyUnnamed()) { + getProperty(element, namingStrategy, new Function() { + @Override + public String apply(final String name) { + return path.apply(name + "[*]"); + } + }, properties, groupProperties); + } + } else if (mapProperty.getValueProperty().isMap()) { + getProperty(mapProperty.getValueProperty(), namingStrategy, + new Function() { + @Override + public String apply(final String name) { + return path.apply(name + ".*"); + } + }, properties, groupProperties); + if (mapProperty.hasKeyUnnamed()) { + getProperty(mapProperty.getValueProperty(), namingStrategy, + new Function() { + @Override + public String apply(final String name) { + return path.apply(name); + } + }, properties, groupProperties); + } + } + } else if (property.isCollection()) { + CollectionProperty collectionProperty = property.asCollection(); + if (collectionProperty.getElement().isLeaf()) { + getProperty(collectionProperty.getElement(), namingStrategy, path, properties, groupProperties); + } + getProperty(collectionProperty.getElement(), namingStrategy, new Function() { + @Override + public String apply(final String name) { + return path.apply(name.endsWith(".*") ? name.substring(0, name.length() - 2) + "[*].*" : name + "[*]"); + } + }, properties, groupProperties); + } else if (property.isOptional()) { + getProperty(property.asOptional().getNestedProperty(), namingStrategy, path, properties, groupProperties); } - - configValidator.validateMapping(type, prefix, value); - - return type.cast(value); } static String getPrefix(Class type) { @@ -137,6 +277,23 @@ public String getPrefix() { return prefix; } + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ConfigClassWithPrefix that = (ConfigClassWithPrefix) o; + return klass.equals(that.klass) && prefix.equals(that.prefix); + } + + @Override + public int hashCode() { + return Objects.hash(klass, prefix); + } + public static ConfigClassWithPrefix configClassWithPrefix(final Class klass, final String prefix) { return new ConfigClassWithPrefix(klass, prefix); } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigSourceContext.java b/implementation/src/main/java/io/smallrye/config/ConfigSourceContext.java index 9e05f07a4..50fc3741f 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigSourceContext.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigSourceContext.java @@ -1,18 +1,59 @@ package io.smallrye.config; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Set; -import io.smallrye.common.annotation.Experimental; +import org.eclipse.microprofile.config.spi.ConfigSource; /** * Exposes contextual information on the ConfigSource initialization via {@link ConfigSourceFactory}. */ -@Experimental("ConfigSource API Enhancements") public interface ConfigSourceContext { ConfigValue getValue(String name); - List getProfiles(); - Iterator iterateNames(); + + default List getProfiles() { + throw new UnsupportedOperationException(); + } + + default List getConfigSources() { + throw new UnsupportedOperationException(); + } + + class ConfigSourceContextConfigSource implements ConfigSource { + private final ConfigSourceContext context; + + public ConfigSourceContextConfigSource(final ConfigSourceContext context) { + this.context = context; + } + + @Override + public Set getPropertyNames() { + Set names = new HashSet<>(); + Iterator namesIterator = context.iterateNames(); + while (namesIterator.hasNext()) { + names.add(namesIterator.next()); + } + return names; + } + + @Override + public String getValue(final String propertyName) { + ConfigValue value = context.getValue(propertyName); + return value != null && value.getValue() != null ? value.getValue() : null; + } + + @Override + public String getName() { + return ConfigSourceContextConfigSource.class.getName(); + } + + @Override + public int getOrdinal() { + return Integer.MAX_VALUE; + } + } } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigSourceFactory.java b/implementation/src/main/java/io/smallrye/config/ConfigSourceFactory.java index 2c8329812..6c8e59937 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigSourceFactory.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigSourceFactory.java @@ -1,11 +1,11 @@ package io.smallrye.config; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.OptionalInt; import org.eclipse.microprofile.config.spi.ConfigSource; -import io.smallrye.common.annotation.Experimental; - /** * This {@code ConfigSourceFactory} allows to initialize a {@link ConfigSource}, with access to the current * {@link ConfigSourceContext}. @@ -22,7 +22,6 @@ * {@code META-INF/services/io.smallrye.config.ConfigSourceFactory} which contains the fully qualified class name of the * custom {@link ConfigSourceFactory} implementation. */ -@Experimental("ConfigSource API Enhancements") public interface ConfigSourceFactory { Iterable getConfigSources(ConfigSourceContext context); @@ -36,4 +35,30 @@ public interface ConfigSourceFactory { default OptionalInt getPriority() { return OptionalInt.empty(); } + + /** + * A {@code ConfigSourceFactory} that accepts a {@link ConfigMapping} interface to configure the + * {@link ConfigSource}. + * + * @param {@link ConfigMapping} interface. + */ + interface ConfigurableConfigSourceFactory extends ConfigSourceFactory { + @Override + @SuppressWarnings("unchecked") + default Iterable getConfigSources(ConfigSourceContext context) { + Type typeArgument = ((ParameterizedType) this.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0]; + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(new ConfigSourceContext.ConfigSourceContextConfigSource(context)) + .withSources(context.getConfigSources()) + .withMapping((Class) typeArgument) + .build(); + + MAPPING mapping = config.getConfigMapping((Class) typeArgument); + + return getConfigSources(context, mapping); + } + + Iterable getConfigSources(ConfigSourceContext context, MAPPING config); + } } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptor.java index fd2a49de1..0f654fa55 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptor.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptor.java @@ -4,8 +4,6 @@ import java.util.Collections; import java.util.Iterator; -import io.smallrye.common.annotation.Experimental; - /** * The ConfigSourceInterceptor allows to intercept the resolution of a configuration name before the * configuration value is resolved by the Config and before any conversion taking place. It can also intercept @@ -24,11 +22,10 @@ *

* * A {@link ConfigSourceInterceptor} implementation class can specify a priority by way of the standard - * {@code javax.annotation.Priority} annotation. If no priority is explicitly assigned, the default priority value + * {@code jakarta.annotation.Priority} annotation. If no priority is explicitly assigned, the default priority value * of {@code io.smallrye.config.Priorities#APPLICATION} is assumed. If multiple interceptors are registered with the * same priority, then their execution order may be non deterministic. */ -@Experimental("Interceptor API to intercept resolution of a configuration name") public interface ConfigSourceInterceptor extends Serializable { /** * Intercept the resolution of a configuration name and either return the corresponding {@link ConfigValue} or a @@ -58,19 +55,6 @@ default Iterator iterateNames(ConfigSourceInterceptorContext context) { return context.iterateNames(); } - /** - * Intercept the resolution of the configuration {@link ConfigValue}. Calling - * {@link ConfigSourceInterceptorContext#iterateNames()} will continue to execute the interceptor chain. The chain - * can be short-circuited by returning another instance of the Iterator. - * - * @param context the interceptor context. See {@link ConfigSourceInterceptorContext} - * - * @return an Iterator of {@link ConfigValue} with information about the name, value, config source and ordinal. - */ - default Iterator iterateValues(ConfigSourceInterceptorContext context) { - return context.iterateValues(); - } - ConfigSourceInterceptor EMPTY = new ConfigSourceInterceptor() { private static final long serialVersionUID = 5749001327530543433L; @@ -83,10 +67,5 @@ public ConfigValue getValue(final ConfigSourceInterceptorContext context, final public Iterator iterateNames(final ConfigSourceInterceptorContext context) { return Collections.emptyIterator(); } - - @Override - public Iterator iterateValues(final ConfigSourceInterceptorContext context) { - return Collections.emptyIterator(); - } }; } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorContext.java b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorContext.java index 88b2bcd65..ab7604b1b 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorContext.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorContext.java @@ -3,34 +3,33 @@ import java.io.Serializable; import java.util.Iterator; -import io.smallrye.common.annotation.Experimental; - /** * Exposes contextual information about the intercepted invocation of {@link ConfigSourceInterceptor}. This allows * implementers to control the behavior of the invocation chain. */ -@Experimental("Interceptor API to intercept resolution of a configuration name") public interface ConfigSourceInterceptorContext extends Serializable { /** * Proceeds to the next interceptor in the chain. * - * @param name the configuration name to lookup. Can be the original key. + * @param name the configuration name to look up (can be the original key) * @return a {@link ConfigValue} with information about the name, value, config source and ordinal, or {@code null} * if the value isn't present. */ ConfigValue proceed(String name); /** - * Proceeds to the next interceptor in the chain. + * Re-calls the first interceptor in the chain. + * If the original name is given, then it is possible to cause a recursive loop, so care must be taken. + * This method is intended to be used by relocating and other compatibility-related interceptors. * - * @return an Iterator of Strings with configuration names. + * @param name the configuration name to look up (can be the original key) + * @return a {@link ConfigValue} with information about the name, value, config source and ordinal, or {@code null} + * if the value isn't present. */ - Iterator iterateNames(); + ConfigValue restart(String name); /** - * Proceeds to the next interceptor in the chain. - * - * @return an Iterator of {@link ConfigValue} with information about the name, value, config source and ordinal. + * @return an iterator over the configuration names known to this interceptor. */ - Iterator iterateValues(); + Iterator iterateNames(); } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorFactory.java b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorFactory.java index db0ad34d3..665974d79 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorFactory.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorFactory.java @@ -2,8 +2,6 @@ import java.util.OptionalInt; -import io.smallrye.common.annotation.Experimental; - /** * This ConfigSourceInterceptorFactory allows to initialize a {@link ConfigSourceInterceptor}, with access to the * current {@link ConfigSourceInterceptorContext}. @@ -19,7 +17,6 @@ * {@code META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory} which contains the fully qualified class * name of the custom {@link ConfigSourceInterceptor} implementation. */ -@Experimental("Interceptor API to intercept resolution of a configuration name") public interface ConfigSourceInterceptorFactory { /** * The default priority value, {@link Priorities#APPLICATION}. diff --git a/implementation/src/main/java/io/smallrye/config/ConfigSourceMap.java b/implementation/src/main/java/io/smallrye/config/ConfigSourceMap.java deleted file mode 100644 index e84cad402..000000000 --- a/implementation/src/main/java/io/smallrye/config/ConfigSourceMap.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2018 Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.smallrye.config; - -import java.io.Serializable; -import java.util.AbstractCollection; -import java.util.AbstractMap; -import java.util.AbstractSet; -import java.util.Collection; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; - -import org.eclipse.microprofile.config.spi.ConfigSource; - -import io.smallrye.common.constraint.Assert; - -/** - * A {@link Map Map<String, String>} which is backed by a {@link ConfigSource}. - * This should not be used to implement {@link ConfigSource#getProperties()} on {@code ConfigSource} - * instances which do not override {@code getPropertyNames()}, as this will result in infinite recursion. - * - * @implNote The key set of the map is the result of calling {@link ConfigSource#getPropertyNames()}; the rest - * of the map operations are derived from this method and {@link ConfigSource#getValue(String)}. - * The values collection and entry set are instantiated lazily and cached. - * The implementation attempts to make no assumptions about the efficiency of the backing implementation and - * prefers the most direct access possible. - *

- * The backing collections are assumed to be immutable. - */ -public class ConfigSourceMap extends AbstractMap implements Map, Serializable { - private static final long serialVersionUID = -6694358608066599032L; - - private final ConfigSource delegate; - private transient Values values; - private transient EntrySet entrySet; - - /** - * Construct a new instance. - * - * @param delegate the delegate configuration source (must not be {@code null}) - */ - public ConfigSourceMap(final ConfigSource delegate) { - this.delegate = Assert.checkNotNullParam("delegate", delegate); - } - - public int size() { - return delegate.getPropertyNames().size(); - } - - public boolean isEmpty() { - // may be cheaper in some cases - return delegate.getPropertyNames().isEmpty(); - } - - public boolean containsKey(final Object key) { - //noinspection SuspiciousMethodCalls - it's OK in this case - return delegate.getPropertyNames().contains(key); - } - - public String get(final Object key) { - return key instanceof String ? delegate.getValue((String) key) : null; - } - - public Set keySet() { - return delegate.getPropertyNames(); - } - - public Collection values() { - Values values = this.values; - if (values == null) - return this.values = new Values(); - return values; - } - - public Set> entrySet() { - EntrySet entrySet = this.entrySet; - if (entrySet == null) - return this.entrySet = new EntrySet(); - return entrySet; - } - - public void forEach(final BiConsumer action) { - // superclass is implemented in terms of entry set - expensive! - for (String name : keySet()) { - action.accept(name, delegate.getValue(name)); - } - } - - final class Values extends AbstractCollection implements Collection { - public Iterator iterator() { - return new Itr(delegate.getPropertyNames().iterator()); - } - - public int size() { - return delegate.getPropertyNames().size(); - } - - public boolean isEmpty() { - // may be cheaper in some cases - return delegate.getPropertyNames().isEmpty(); - } - - final class Itr implements Iterator { - private final Iterator iterator; - - Itr(final Iterator iterator) { - this.iterator = iterator; - } - - public boolean hasNext() { - return iterator.hasNext(); - } - - public String next() { - return delegate.getValue(iterator.next()); - } - } - } - - final class EntrySet extends AbstractSet> { - public Iterator> iterator() { - return new Itr(delegate.getPropertyNames().iterator()); - } - - public int size() { - return delegate.getPropertyNames().size(); - } - - public boolean isEmpty() { - // may be cheaper in some cases - return delegate.getPropertyNames().isEmpty(); - } - - final class Itr implements Iterator> { - private final Iterator iterator; - - Itr(final Iterator iterator) { - this.iterator = iterator; - } - - public boolean hasNext() { - return iterator.hasNext(); - } - - public Entry next() { - String name = iterator.next(); - return new SimpleImmutableEntry<>(name, delegate.getValue(name)); - } - } - } -} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValidationException.java b/implementation/src/main/java/io/smallrye/config/ConfigValidationException.java index bcd8957fb..07c0a94b7 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigValidationException.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigValidationException.java @@ -1,6 +1,7 @@ package io.smallrye.config; import java.io.Serializable; +import java.util.Optional; import io.smallrye.common.constraint.Assert; @@ -18,13 +19,13 @@ public class ConfigValidationException extends RuntimeException { * @param problems the reported problems */ public ConfigValidationException(final Problem[] problems) { - super(list("Configuration validation failed", problems)); + super(list(problems)); this.problems = problems; } - private static String list(String msg, Problem[] problems) { + private static String list(Problem[] problems) { StringBuilder b = new StringBuilder(); - b.append(msg).append(':'); + b.append("Configuration validation failed").append(':'); for (int i = 0; i < problems.length; i++) { Problem problem = problems[i]; Assert.checkNotNullArrayParam("problems", i, problem); @@ -48,13 +49,24 @@ public static final class Problem implements Serializable { private static final long serialVersionUID = 5984436393578154541L; private final String message; + transient private final RuntimeException exception; public Problem(final String message) { this.message = message; + this.exception = null; + } + + Problem(final RuntimeException exception) { + this.message = exception.getMessage(); + this.exception = exception; } public String getMessage() { return message; } + + Optional getException() { + return Optional.ofNullable(exception); + } } } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValue.java b/implementation/src/main/java/io/smallrye/config/ConfigValue.java index a94a369a1..8da1e35b5 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigValue.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigValue.java @@ -1,9 +1,15 @@ package io.smallrye.config; +import static io.smallrye.config.ProfileConfigSourceInterceptor.convertProfile; +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; + +import java.util.ArrayList; import java.util.Comparator; +import java.util.List; import java.util.Objects; -import io.smallrye.common.annotation.Experimental; +import io.smallrye.config.ConfigValidationException.Problem; /** * The ConfigValue is a metadata object that holds additional information after the lookup of a configuration. @@ -17,7 +23,6 @@ * This is used together with {@link ConfigValueConfigSource} and {@link ConfigSourceInterceptor} to expose the * Configuration lookup metadata. */ -@Experimental("Extension to the original ConfigSource to allow retrieval of additional metadata on config lookup") public class ConfigValue implements org.eclipse.microprofile.config.ConfigValue { private final String name; private final String value; @@ -28,6 +33,9 @@ public class ConfigValue implements org.eclipse.microprofile.config.ConfigValue private final int configSourcePosition; private final int lineNumber; + private final String extendedExpressionHandler; + private final List problems; + private ConfigValue(final ConfigValueBuilder builder) { this.name = builder.name; this.value = builder.value; @@ -37,6 +45,8 @@ private ConfigValue(final ConfigValueBuilder builder) { this.configSourceOrdinal = builder.configSourceOrdinal; this.configSourcePosition = builder.configSourcePosition; this.lineNumber = builder.lineNumber; + this.extendedExpressionHandler = builder.extendedExpressionHandler; + this.problems = builder.problems; } @Override @@ -92,6 +102,18 @@ public String getLocation() { return lineNumber != -1 ? configSourceName + ":" + lineNumber : configSourceName; } + public String getExtendedExpressionHandler() { + return extendedExpressionHandler; + } + + boolean hasProblems() { + return problems != null && !problems.isEmpty(); + } + + List getProblems() { + return hasProblems() ? unmodifiableList(problems) : emptyList(); + } + public ConfigValue withName(final String name) { return from().withName(name).build(); } @@ -120,6 +142,18 @@ public ConfigValue withLineNumber(final int lineNumber) { return from().withLineNumber(lineNumber).build(); } + public ConfigValue withExtendedExpressionHandler(final String extendedExpressionHandler) { + return from().withExtendedExpressionHandler(extendedExpressionHandler).build(); + } + + public ConfigValue noProblems() { + return from().noProblems().build(); + } + + public ConfigValue withProblems(final List problems) { + return from().withProblems(problems).build(); + } + @Override public boolean equals(final Object o) { if (this == o) { @@ -157,7 +191,7 @@ public String toString() { '}'; } - ConfigValueBuilder from() { + public ConfigValueBuilder from() { return new ConfigValueBuilder() .withName(name) .withValue(value) @@ -166,7 +200,9 @@ ConfigValueBuilder from() { .withConfigSourceName(configSourceName) .withConfigSourceOrdinal(configSourceOrdinal) .withConfigSourcePosition(configSourcePosition) - .withLineNumber(lineNumber); + .withLineNumber(lineNumber) + .withExtendedExpressionHandler(extendedExpressionHandler) + .withProblems(problems); } public static ConfigValueBuilder builder() { @@ -182,6 +218,8 @@ public static class ConfigValueBuilder { private int configSourceOrdinal; private int configSourcePosition; private int lineNumber = -1; + private String extendedExpressionHandler; + private List problems; public ConfigValueBuilder withName(final String name) { this.name = name; @@ -223,7 +261,38 @@ public ConfigValueBuilder withLineNumber(final int lineNumber) { return this; } + public ConfigValueBuilder withExtendedExpressionHandler(final String extendedExpressionHandler) { + this.extendedExpressionHandler = extendedExpressionHandler; + return this; + } + + public ConfigValueBuilder noProblems() { + this.problems = emptyList(); + return this; + } + + public ConfigValueBuilder withProblems(final List problems) { + if (problems != null) { + if (this.problems == null) { + this.problems = new ArrayList<>(); + } + this.problems.addAll(problems); + } + return this; + } + + public ConfigValueBuilder addProblem(final Problem problem) { + if (this.problems == null) { + this.problems = new ArrayList<>(); + } + this.problems.add(problem); + return this; + } + public ConfigValue build() { + if (problems != null && !problems.isEmpty()) { + this.value = null; + } return new ConfigValue(this); } } @@ -235,7 +304,19 @@ public int compare(ConfigValue original, ConfigValue candidate) { if (result != 0) { return result; } - return Integer.compare(original.configSourcePosition, candidate.configSourcePosition) * -1; + result = Integer.compare(original.configSourcePosition, candidate.configSourcePosition) * -1; + if (result != 0) { + return result; + } + // If both properties are profiled, prioritize the one with the most specific profile. + if (original.getName().charAt(0) == '%' && candidate.getName().charAt(0) == '%') { + List originalProfiles = convertProfile( + new NameIterator(original.getName()).getNextSegment().substring(1)); + List candidateProfiles = convertProfile( + new NameIterator(candidate.getName()).getNextSegment().substring(1)); + return Integer.compare(originalProfiles.size(), candidateProfiles.size()) * -1; + } + return result; } }; } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValueConfigSourceWrapper.java b/implementation/src/main/java/io/smallrye/config/ConfigValueConfigSourceWrapper.java deleted file mode 100644 index 45b35aef4..000000000 --- a/implementation/src/main/java/io/smallrye/config/ConfigValueConfigSourceWrapper.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.smallrye.config; - -import java.io.Serializable; -import java.util.Map; -import java.util.Set; - -import org.eclipse.microprofile.config.spi.ConfigSource; - -final class ConfigValueConfigSourceWrapper implements ConfigValueConfigSource, Serializable { - private static final long serialVersionUID = -1109094614437147326L; - - private final ConfigSource configSource; - - private ConfigValueConfigSourceWrapper(final ConfigSource configSource) { - this.configSource = configSource; - } - - @Override - public ConfigValue getConfigValue(final String propertyName) { - String value = configSource.getValue(propertyName); - if (value != null) { - return ConfigValue.builder() - .withName(propertyName) - .withValue(value) - .withRawValue(value) - .withConfigSourceName(getName()) - .withConfigSourceOrdinal(getOrdinal()) - .build(); - } - - return null; - } - - @Override - public Map getConfigValueProperties() { - return new ConfigValueMapStringView(configSource.getProperties(), - configSource.getName(), - configSource.getOrdinal()); - } - - @Override - public Map getProperties() { - return configSource.getProperties(); - } - - @Override - public String getValue(final String propertyName) { - return configSource.getValue(propertyName); - } - - @Override - public Set getPropertyNames() { - return configSource.getPropertyNames(); - } - - @Override - public String getName() { - return configSource.getName(); - } - - @Override - public int getOrdinal() { - return configSource.getOrdinal(); - } - - ConfigSource unwrap() { - return configSource; - } - - static ConfigValueConfigSource wrap(final ConfigSource configSource) { - if (configSource instanceof ConfigValueConfigSource) { - return (ConfigValueConfigSource) configSource; - } else { - return new ConfigValueConfigSourceWrapper(configSource); - } - } -} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValueConverter.java b/implementation/src/main/java/io/smallrye/config/ConfigValueConverter.java deleted file mode 100644 index 0f5ff2093..000000000 --- a/implementation/src/main/java/io/smallrye/config/ConfigValueConverter.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.smallrye.config; - -import org.eclipse.microprofile.config.spi.Converter; - -/** - * This Converter should not be used directly. This is only used as a marker to use to return a ConfigValue directly - * after a configuration property lookup. - */ -class ConfigValueConverter implements Converter { - static final Converter CONFIG_VALUE_CONVERTER = new ConfigValueConverter(); - - @Override - public ConfigValue convert(final String value) { - throw new IllegalArgumentException(); - } -} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValueProperties.java b/implementation/src/main/java/io/smallrye/config/ConfigValueProperties.java deleted file mode 100644 index b7c9891bb..000000000 --- a/implementation/src/main/java/io/smallrye/config/ConfigValueProperties.java +++ /dev/null @@ -1,310 +0,0 @@ -package io.smallrye.config; - -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.util.HashMap; - -/** - * Loads properties as {@link ConfigValue}. - *

- * - * This class is mostly a subset copy of {@link java.util.Properties}. This was required to be able to keep track of - * the line number from which the configuration was loaded. - */ -class ConfigValueProperties extends HashMap { - private final String configSourceName; - private final int configSourceOrdinal; - - public ConfigValueProperties(final String configSourceName, final int configSourceOrdinal) { - this.configSourceName = configSourceName; - this.configSourceOrdinal = configSourceOrdinal; - } - - public synchronized void load(Reader reader) throws IOException { - load0(new LineReader(reader)); - } - - public synchronized void load(InputStream inStream) throws IOException { - load0(new LineReader(inStream)); - } - - private void load0(LineReader lr) throws IOException { - char[] convtBuf = new char[1024]; - int limit; - int keyLen; - int valueStart; - char c; - boolean hasSep; - boolean precedingBackslash; - - while ((limit = lr.readLine()) >= 0) { - c = 0; - keyLen = 0; - valueStart = limit; - hasSep = false; - - //System.out.println("line=<" + new String(lineBuf, 0, limit) + ">"); - precedingBackslash = false; - while (keyLen < limit) { - c = lr.lineBuf[keyLen]; - //need check if escaped. - if ((c == '=' || c == ':') && !precedingBackslash) { - valueStart = keyLen + 1; - hasSep = true; - break; - } else if ((c == ' ' || c == '\t' || c == '\f') && !precedingBackslash) { - valueStart = keyLen + 1; - break; - } - if (c == '\\') { - precedingBackslash = !precedingBackslash; - } else { - precedingBackslash = false; - } - keyLen++; - } - while (valueStart < limit) { - c = lr.lineBuf[valueStart]; - if (c != ' ' && c != '\t' && c != '\f') { - if (!hasSep && (c == '=' || c == ':')) { - hasSep = true; - } else { - break; - } - } - valueStart++; - } - String key = loadConvert(lr.lineBuf, 0, keyLen, convtBuf); - String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, convtBuf); - put(key, ConfigValue.builder() - .withName(key) - .withValue(value) - .withRawValue(value) - .withConfigSourceName(configSourceName) - .withConfigSourceOrdinal(configSourceOrdinal) - .withLineNumber(lr.lineNumber) - .build()); - } - } - - class LineReader { - public LineReader(InputStream inStream) { - this.inStream = inStream; - inByteBuf = new byte[8192]; - } - - public LineReader(Reader reader) { - this.reader = reader; - inCharBuf = new char[8192]; - } - - byte[] inByteBuf; - char[] inCharBuf; - char[] lineBuf = new char[1024]; - int inLimit = 0; - int inOff = 0; - InputStream inStream; - Reader reader; - int lineNumber = 0; - int addBackslash = 0; - - int readLine() throws IOException { - int len = 0; - char c = 0; - - boolean skipWhiteSpace = true; - boolean isCommentLine = false; - boolean isNewLine = true; - boolean appendedLineBegin = false; - boolean precedingBackslash = false; - boolean skipLF = false; - lineNumber = ++lineNumber + addBackslash; - addBackslash = 0; - - while (true) { - if (inOff >= inLimit) { - inLimit = (inStream == null) ? reader.read(inCharBuf) - : inStream.read(inByteBuf); - inOff = 0; - if (inLimit <= 0) { - if (len == 0 || isCommentLine) { - return -1; - } - if (precedingBackslash) { - len--; - } - return len; - } - } - if (inStream != null) { - //The line below is equivalent to calling a - //ISO8859-1 decoder. - c = (char) (0xff & inByteBuf[inOff++]); - } else { - c = inCharBuf[inOff++]; - } - if (skipLF) { - skipLF = false; - if (c == '\n') { - continue; - } - } - if (skipWhiteSpace) { - if (c == ' ' || c == '\t' || c == '\f') { - continue; - } - if (!appendedLineBegin && (c == '\r' || c == '\n')) { - if (c == '\n') { - lineNumber++; - } - continue; - } - skipWhiteSpace = false; - appendedLineBegin = false; - } - if (isNewLine) { - isNewLine = false; - if (c == '#' || c == '!') { - isCommentLine = true; - continue; - } - } - - if (c != '\n' && c != '\r') { - lineBuf[len++] = c; - if (len == lineBuf.length) { - int newLength = lineBuf.length * 2; - if (newLength < 0) { - newLength = Integer.MAX_VALUE; - } - char[] buf = new char[newLength]; - System.arraycopy(lineBuf, 0, buf, 0, lineBuf.length); - lineBuf = buf; - } - //flip the preceding backslash flag - if (c == '\\') { - precedingBackslash = !precedingBackslash; - } else { - precedingBackslash = false; - } - } else { - // reached EOL - if (isCommentLine || len == 0) { - isCommentLine = false; - isNewLine = true; - skipWhiteSpace = true; - len = 0; - lineNumber++; - continue; - } - if (inOff >= inLimit) { - inLimit = (inStream == null) - ? reader.read(inCharBuf) - : inStream.read(inByteBuf); - inOff = 0; - if (inLimit <= 0) { - if (precedingBackslash) { - len--; - } - return len; - } - } - if (precedingBackslash) { - len -= 1; - //skip the leading whitespace characters in following line - skipWhiteSpace = true; - appendedLineBegin = true; - precedingBackslash = false; - addBackslash++; - if (c == '\r') { - skipLF = true; - } - } else { - if (c == '\r') { - inOff++; - } - - return len; - } - } - } - } - } - - private String loadConvert(char[] in, int off, int len, char[] convtBuf) { - if (convtBuf.length < len) { - int newLen = len * 2; - if (newLen < 0) { - newLen = Integer.MAX_VALUE; - } - convtBuf = new char[newLen]; - } - char aChar; - char[] out = convtBuf; - int outLen = 0; - int end = off + len; - - while (off < end) { - aChar = in[off++]; - if (aChar == '\\') { - aChar = in[off++]; - if (aChar == 'u') { - // Read the xxxx - int value = 0; - for (int i = 0; i < 4; i++) { - aChar = in[off++]; - switch (aChar) { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - value = (value << 4) + aChar - '0'; - break; - case 'a': - case 'b': - case 'c': - case 'd': - case 'e': - case 'f': - value = (value << 4) + 10 + aChar - 'a'; - break; - case 'A': - case 'B': - case 'C': - case 'D': - case 'E': - case 'F': - value = (value << 4) + 10 + aChar - 'A'; - break; - default: - throw ConfigMessages.msg.malformedEncoding(); - } - } - out[outLen++] = (char) value; - } else { - if (aChar == 't') { - aChar = '\t'; - } else if (aChar == 'r') { - aChar = '\r'; - } else if (aChar == 'n') { - aChar = '\n'; - } else if (aChar == 'f') { - aChar = '\f'; - } - out[outLen++] = aChar; - } - } else { - out[outLen++] = aChar; - } - } - return new String(out, 0, outLen); - } - -} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValuePropertiesConfigSource.java b/implementation/src/main/java/io/smallrye/config/ConfigValuePropertiesConfigSource.java index f280fc07e..c5b81cca4 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigValuePropertiesConfigSource.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigValuePropertiesConfigSource.java @@ -1,13 +1,18 @@ package io.smallrye.config; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; +import java.io.Reader; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; +import io.smallrye.config._private.ConfigMessages; import io.smallrye.config.common.utils.ConfigSourceUtil; +@Deprecated(forRemoval = true) public class ConfigValuePropertiesConfigSource extends MapBackedConfigValueConfigSource { private static final long serialVersionUID = 9070158352250209380L; @@ -39,4 +44,309 @@ private static Map urlToConfigValueMap(URL locationOfProper return p; } } + + /** + * Loads properties as {@link ConfigValue}. + *

+ * + * This class is mostly a subset copy of {@link java.util.Properties}. This was required to be able to keep track of + * the line number from which the configuration was loaded. + */ + static class ConfigValueProperties extends HashMap { + private static final long serialVersionUID = 613423366086278005L; + private final String configSourceName; + private final int configSourceOrdinal; + + public ConfigValueProperties(final String configSourceName, final int configSourceOrdinal) { + this.configSourceName = configSourceName; + this.configSourceOrdinal = configSourceOrdinal; + } + + public synchronized void load(Reader reader) throws IOException { + load0(new LineReader(reader)); + } + + public synchronized void load(InputStream inStream) throws IOException { + load0(new LineReader(inStream)); + } + + private void load0(LineReader lr) throws IOException { + char[] convtBuf = new char[1024]; + int limit; + int keyLen; + int valueStart; + char c; + boolean hasSep; + boolean precedingBackslash; + + while ((limit = lr.readLine()) >= 0) { + c = 0; + keyLen = 0; + valueStart = limit; + hasSep = false; + + //System.out.println("line=<" + new String(lineBuf, 0, limit) + ">"); + precedingBackslash = false; + while (keyLen < limit) { + c = lr.lineBuf[keyLen]; + //need check if escaped. + if ((c == '=' || c == ':') && !precedingBackslash) { + valueStart = keyLen + 1; + hasSep = true; + break; + } else if ((c == ' ' || c == '\t' || c == '\f') && !precedingBackslash) { + valueStart = keyLen + 1; + break; + } + if (c == '\\') { + precedingBackslash = !precedingBackslash; + } else { + precedingBackslash = false; + } + keyLen++; + } + while (valueStart < limit) { + c = lr.lineBuf[valueStart]; + if (c != ' ' && c != '\t' && c != '\f') { + if (!hasSep && (c == '=' || c == ':')) { + hasSep = true; + } else { + break; + } + } + valueStart++; + } + String key = loadConvert(lr.lineBuf, 0, keyLen, convtBuf); + String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, convtBuf); + put(key, ConfigValue.builder() + .withName(key) + .withValue(value) + .withRawValue(value) + .withConfigSourceName(configSourceName) + .withConfigSourceOrdinal(configSourceOrdinal) + .withLineNumber(lr.lineNumber) + .build()); + } + } + + class LineReader { + public LineReader(InputStream inStream) { + this.inStream = inStream; + inByteBuf = new byte[8192]; + } + + public LineReader(Reader reader) { + this.reader = reader; + inCharBuf = new char[8192]; + } + + byte[] inByteBuf; + char[] inCharBuf; + char[] lineBuf = new char[1024]; + int inLimit = 0; + int inOff = 0; + InputStream inStream; + Reader reader; + int lineNumber = 0; + int addBackslash = 0; + + int readLine() throws IOException { + int len = 0; + char c = 0; + + boolean skipWhiteSpace = true; + boolean isCommentLine = false; + boolean isNewLine = true; + boolean appendedLineBegin = false; + boolean precedingBackslash = false; + boolean skipLF = false; + lineNumber = ++lineNumber + addBackslash; + addBackslash = 0; + + while (true) { + if (inOff >= inLimit) { + inLimit = (inStream == null) ? reader.read(inCharBuf) + : inStream.read(inByteBuf); + inOff = 0; + if (inLimit <= 0) { + if (len == 0 || isCommentLine) { + return -1; + } + if (precedingBackslash) { + len--; + } + return len; + } + } + if (inStream != null) { + //The line below is equivalent to calling a + //ISO8859-1 decoder. + c = (char) (0xff & inByteBuf[inOff++]); + } else { + c = inCharBuf[inOff++]; + } + if (skipLF) { + skipLF = false; + if (c == '\n') { + continue; + } + } + if (skipWhiteSpace) { + if (c == ' ' || c == '\t' || c == '\f') { + continue; + } + if (!appendedLineBegin && (c == '\r' || c == '\n')) { + if (c == '\n') { + lineNumber++; + } + continue; + } + skipWhiteSpace = false; + appendedLineBegin = false; + } + if (isNewLine) { + isNewLine = false; + if (c == '#' || c == '!') { + isCommentLine = true; + continue; + } + } + + if (c != '\n' && c != '\r') { + lineBuf[len++] = c; + if (len == lineBuf.length) { + int newLength = lineBuf.length * 2; + if (newLength < 0) { + newLength = Integer.MAX_VALUE; + } + char[] buf = new char[newLength]; + System.arraycopy(lineBuf, 0, buf, 0, lineBuf.length); + lineBuf = buf; + } + //flip the preceding backslash flag + if (c == '\\') { + precedingBackslash = !precedingBackslash; + } else { + precedingBackslash = false; + } + } else { + // reached EOL + if (isCommentLine || len == 0) { + isCommentLine = false; + isNewLine = true; + skipWhiteSpace = true; + len = 0; + lineNumber++; + continue; + } + if (inOff >= inLimit) { + inLimit = (inStream == null) + ? reader.read(inCharBuf) + : inStream.read(inByteBuf); + inOff = 0; + if (inLimit <= 0) { + if (precedingBackslash) { + len--; + } + return len; + } + } + if (precedingBackslash) { + len -= 1; + //skip the leading whitespace characters in following line + skipWhiteSpace = true; + appendedLineBegin = true; + precedingBackslash = false; + addBackslash++; + if (c == '\r') { + skipLF = true; + } + } else { + if (c == '\r') { + inOff++; + } + + return len; + } + } + } + } + } + + private String loadConvert(char[] in, int off, int len, char[] convtBuf) { + if (convtBuf.length < len) { + int newLen = len * 2; + if (newLen < 0) { + newLen = Integer.MAX_VALUE; + } + convtBuf = new char[newLen]; + } + char aChar; + char[] out = convtBuf; + int outLen = 0; + int end = off + len; + + while (off < end) { + aChar = in[off++]; + if (aChar == '\\') { + aChar = in[off++]; + if (aChar == 'u') { + // Read the xxxx + int value = 0; + for (int i = 0; i < 4; i++) { + aChar = in[off++]; + switch (aChar) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + value = (value << 4) + aChar - '0'; + break; + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + value = (value << 4) + 10 + aChar - 'a'; + break; + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + value = (value << 4) + 10 + aChar - 'A'; + break; + default: + throw ConfigMessages.msg.malformedEncoding(); + } + } + out[outLen++] = (char) value; + } else { + if (aChar == 't') { + aChar = '\t'; + } else if (aChar == 'r') { + aChar = '\r'; + } else if (aChar == 'n') { + aChar = '\n'; + } else if (aChar == 'f') { + aChar = '\f'; + } + out[outLen++] = aChar; + } + } else { + out[outLen++] = aChar; + } + } + return new String(out, 0, outLen); + } + + } } diff --git a/implementation/src/main/java/io/smallrye/config/Converters.java b/implementation/src/main/java/io/smallrye/config/Converters.java index 9c3e2c639..969e05042 100644 --- a/implementation/src/main/java/io/smallrye/config/Converters.java +++ b/implementation/src/main/java/io/smallrye/config/Converters.java @@ -16,6 +16,8 @@ package io.smallrye.config; +import static io.smallrye.config.common.utils.StringUtil.unquoted; + import java.io.ObjectStreamException; import java.io.Serializable; import java.lang.reflect.Array; @@ -23,6 +25,7 @@ import java.lang.reflect.Type; import java.net.InetAddress; import java.net.UnknownHostException; +import java.nio.file.Path; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; @@ -42,6 +45,7 @@ import org.eclipse.microprofile.config.spi.Converter; +import io.smallrye.config._private.ConfigMessages; import io.smallrye.config.common.AbstractConverter; import io.smallrye.config.common.AbstractDelegatingConverter; import io.smallrye.config.common.AbstractSimpleDelegatingConverter; @@ -56,6 +60,8 @@ public final class Converters { private Converters() { } + static final Converter CONFIG_VALUE_CONVERTER = new ConfigValueConverter(); + static final Converter STRING_CONVERTER = BuiltInConverter.of(0, newEmptyValueConverter(value -> value)); static final Converter BOOLEAN_CONVERTER = BuiltInConverter.of(1, newTrimmingConverter(newEmptyValueConverter( @@ -173,6 +179,9 @@ private Converters() { static final Converter PATTERN_CONVERTER = BuiltInConverter.of(17, newTrimmingConverter(newEmptyValueConverter(Pattern::compile))); + static final Converter PATH_CONVERTER = BuiltInConverter.of(18, + newEmptyValueConverter(Path::of)); + static final Map, Class> PRIMITIVE_TYPES; static final Map> ALL_CONVERTERS = new HashMap<>(); @@ -211,6 +220,8 @@ private Converters() { ALL_CONVERTERS.put(Pattern.class, PATTERN_CONVERTER); + ALL_CONVERTERS.put(Path.class, PATH_CONVERTER); + Map, Class> primitiveTypes = new HashMap<>(9); primitiveTypes.put(byte.class, Byte.class); primitiveTypes.put(short.class, Short.class); @@ -309,13 +320,14 @@ public static Converter newArrayConverter(Converter itemC * * @param keyConverter the converter used to convert the keys * @param valueConverter the converter used to convert the values + * @param mapFactory the map factory (must not be {@code null}) + * @return the new converter (not {@code null}) * @param the type of the keys * @param the type of the values - * @return the new converter (not {@code null}) */ public static Converter> newMapConverter(Converter keyConverter, - Converter valueConverter) { - return new MapConverter<>(keyConverter, valueConverter); + Converter valueConverter, IntFunction> mapFactory) { + return new MapConverter<>(keyConverter, valueConverter, mapFactory); } /** @@ -617,6 +629,26 @@ public static Converter patternValidatingConverter(Converter return patternValidatingConverter(delegate, Pattern.compile(pattern)); } + static boolean isOptionalConverter(Converter converter) { + return converter instanceof Converters.OptionalConverter || + converter.equals(Converters.OPTIONAL_INT_CONVERTER) || + converter.equals(Converters.OPTIONAL_LONG_CONVERTER) || + converter.equals(Converters.OPTIONAL_DOUBLE_CONVERTER); + } + + /** + * This Converter should not be used directly. This is only used as a marker to use to return a ConfigValue directly + * after a configuration property lookup. + */ + static final class ConfigValueConverter implements Converter { + private static final long serialVersionUID = -5005688684588039934L; + + @Override + public ConfigValue convert(final String value) throws IllegalArgumentException, NullPointerException { + throw new IllegalStateException(); + } + } + static final class PatternCheckConverter implements Converter, Serializable { private static final long serialVersionUID = 358813973126582008L; @@ -709,7 +741,7 @@ static final class CollectionConverter> extends Abstr this.collectionFactory = collectionFactory; } - protected Converter create(final Converter newDelegate) { + Converter create(final Converter newDelegate) { return new CollectionConverter<>(newDelegate, collectionFactory); } @@ -742,7 +774,7 @@ static final class ArrayConverter extends AbstractDelegatingConverter create(final Converter newDelegate) { + ArrayConverter create(final Converter newDelegate) { return new ArrayConverter<>(newDelegate, arrayType); } @@ -797,8 +829,8 @@ static final class OptionalConverter extends AbstractDelegatingConverter create(final Converter newDelegate) { - return new OptionalConverter(newDelegate); + OptionalConverter create(final Converter newDelegate) { + return new OptionalConverter<>(newDelegate); } public Optional convert(final String value) { @@ -817,11 +849,11 @@ public Optional convert(final String value) { static final class OptionalIntConverter extends AbstractDelegatingConverter { private static final long serialVersionUID = 4331039532024222756L; - protected OptionalIntConverter(final Converter delegate) { + OptionalIntConverter(final Converter delegate) { super(delegate); } - protected OptionalIntConverter create(final Converter newDelegate) { + OptionalIntConverter create(final Converter newDelegate) { return new OptionalIntConverter(newDelegate); } @@ -830,7 +862,7 @@ public OptionalInt convert(final String value) { return OptionalInt.empty(); } else { final Integer converted = getDelegate().convert(value); - return converted == null ? OptionalInt.empty() : OptionalInt.of(converted.intValue()); + return converted == null ? OptionalInt.empty() : OptionalInt.of(converted); } } } @@ -838,11 +870,11 @@ public OptionalInt convert(final String value) { static final class OptionalLongConverter extends AbstractDelegatingConverter { private static final long serialVersionUID = 140937551800590852L; - protected OptionalLongConverter(final Converter delegate) { + OptionalLongConverter(final Converter delegate) { super(delegate); } - protected OptionalLongConverter create(final Converter newDelegate) { + OptionalLongConverter create(final Converter newDelegate) { return new OptionalLongConverter(newDelegate); } @@ -851,7 +883,7 @@ public OptionalLong convert(final String value) { return OptionalLong.empty(); } else { final Long converted = getDelegate().convert(value); - return converted == null ? OptionalLong.empty() : OptionalLong.of(converted.longValue()); + return converted == null ? OptionalLong.empty() : OptionalLong.of(converted); } } } @@ -863,7 +895,7 @@ static final class OptionalDoubleConverter extends AbstractDelegatingConverter newDelegate) { + OptionalDoubleConverter create(final Converter newDelegate) { return new OptionalDoubleConverter(newDelegate); } @@ -872,7 +904,7 @@ public OptionalDouble convert(final String value) { return OptionalDouble.empty(); } else { final Double converted = getDelegate().convert(value); - return converted == null ? OptionalDouble.empty() : OptionalDouble.of(converted.doubleValue()); + return converted == null ? OptionalDouble.empty() : OptionalDouble.of(converted); } } } @@ -1021,16 +1053,22 @@ static class MapConverter extends AbstractConverter> { * The converter to use the for values. */ private final Converter valueConverter; + private final IntFunction> mapFactory; /** * Construct a {@code MapConverter} with the given converters. - * + * * @param keyConverter the converter to use the for keys * @param valueConverter the converter to use the for values + * @param mapFactory */ - MapConverter(Converter keyConverter, Converter valueConverter) { + MapConverter( + final Converter keyConverter, + final Converter valueConverter, + final IntFunction> mapFactory) { this.keyConverter = keyConverter; this.valueConverter = valueConverter; + this.mapFactory = mapFactory; } @Override @@ -1038,8 +1076,8 @@ public Map convert(String value) throws IllegalArgumentException, NullPoin if (value == null) { return null; } - final Map map = new HashMap<>(); - final StringBuilder currentLine = new StringBuilder(value.length()); + Map map = mapFactory.apply(0); + StringBuilder currentLine = new StringBuilder(value.length()); int fromIndex = 0; for (int idx; (idx = value.indexOf(';', fromIndex)) >= 0; fromIndex = idx + 1) { if (value.charAt(idx - 1) == '\\') { @@ -1059,38 +1097,34 @@ public Map convert(String value) throws IllegalArgumentException, NullPoin /** * Converts the line into an entry and add it to the given map. - * + * * @param map the map to which the extracted entries are added. * @param value the original value to convert. * @param rawLine the extracted line to convert into an entry. * @throws NoSuchElementException if the line could not be converted into an entry or doesn't have the expected format. */ private void processLine(Map map, String value, String rawLine) { - final String line = rawLine.replace("\\;", ";"); + String line = rawLine.replace("\\;", ";"); for (int idx, fromIndex = 0; (idx = line.indexOf('=', fromIndex)) >= 0; fromIndex = idx + 1) { if (line.charAt(idx - 1) == '\\') { // The key separator has been escaped continue; } - processEntry(map, line.substring(0, idx).replace("\\=", "="), line.substring(idx + 1).replace("\\=", "=")); + processEntry(map, unquoted(line.substring(0, idx).replace("\\=", "=")), + line.substring(idx + 1).replace("\\=", "=")); return; } throw ConfigMessages.msg.valueNotMatchMapFormat(value); } /** - * Converts the key/value pair into the expected format and add it to the given map. It will ignore - * keys with sub namespace. + * Converts the key/value pair into the expected format and add it to the given map. * * @param map the map to which the key/value pair is added. * @param key the key of the key/value pair to add to the map * @param value the value of the key/value pair to add to the map */ private void processEntry(Map map, String key, String value) { - if (key.indexOf('.') >= 0) { - // Ignore sub namespaces - return; - } map.put(keyConverter.convert(key), valueConverter.convert(value)); } } diff --git a/implementation/src/main/java/io/smallrye/config/DefaultValuesConfigSource.java b/implementation/src/main/java/io/smallrye/config/DefaultValuesConfigSource.java index 8cf6df0a9..b9c14cdf0 100644 --- a/implementation/src/main/java/io/smallrye/config/DefaultValuesConfigSource.java +++ b/implementation/src/main/java/io/smallrye/config/DefaultValuesConfigSource.java @@ -1,17 +1,49 @@ package io.smallrye.config; +import java.util.HashMap; import java.util.Map; +import java.util.Set; -public class DefaultValuesConfigSource extends KeyMapBackedConfigSource { +import io.smallrye.config.common.AbstractConfigSource; + +public final class DefaultValuesConfigSource extends AbstractConfigSource { private static final long serialVersionUID = -6386021034957868328L; - public DefaultValuesConfigSource(final KeyMap properties) { - super("DefaultValuesConfigSource", Integer.MIN_VALUE, properties); + private final Map properties; + + private final Map wildcards; + + public DefaultValuesConfigSource(final Map properties) { + this(properties, "DefaultValuesConfigSource", Integer.MIN_VALUE); + } + + public DefaultValuesConfigSource(final Map properties, final String name, final int ordinal) { + super(name, ordinal); + this.properties = new HashMap<>(); + this.wildcards = new HashMap<>(); + addDefaults(properties); + } + + @Override + public Set getPropertyNames() { + return properties.keySet(); + } + + public String getValue(final String propertyName) { + return properties.getOrDefault(propertyName, wildcards.get(new PropertyName(propertyName))); + } + + void addDefaults(final Map properties) { + for (Map.Entry entry : properties.entrySet()) { + addDefault(entry.getKey(), entry.getValue()); + } } - void registerDefaults(final KeyMap properties) { - for (Map.Entry> entry : properties.entrySet()) { - getKeyMapProperties().put(entry.getKey(), entry.getValue()); + void addDefault(final String name, final String value) { + if (name.indexOf('*') == -1) { + this.properties.putIfAbsent(name, value); + } else { + this.wildcards.put(new PropertyName(name), value); } } } diff --git a/implementation/src/main/java/io/smallrye/config/DotEnvConfigSourceProvider.java b/implementation/src/main/java/io/smallrye/config/DotEnvConfigSourceProvider.java index f2d418fac..7ccff65f6 100644 --- a/implementation/src/main/java/io/smallrye/config/DotEnvConfigSourceProvider.java +++ b/implementation/src/main/java/io/smallrye/config/DotEnvConfigSourceProvider.java @@ -2,19 +2,24 @@ import java.io.IOException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.ConfigSourceProvider; import io.smallrye.config.common.utils.ConfigSourceUtil; +import io.smallrye.config.common.utils.StringUtil; public class DotEnvConfigSourceProvider extends AbstractLocationConfigSourceLoader implements ConfigSourceProvider { private final String location; public DotEnvConfigSourceProvider() { - this(Paths.get(System.getProperty("user.dir"), ".env").toUri().toString()); + this(getDotEnvFile(".env")); } public DotEnvConfigSourceProvider(final String location) { @@ -28,7 +33,11 @@ protected String[] getFileExtensions() { @Override protected ConfigSource loadConfigSource(final URL url, final int ordinal) throws IOException { - return new EnvConfigSource(ConfigSourceUtil.urlToMap(url), ordinal) { + Map envProperties = new HashMap<>(); + for (final Map.Entry entry : ConfigSourceUtil.urlToMap(url).entrySet()) { + envProperties.put(StringUtil.replaceNonAlphanumericByUnderscores(entry.getKey()), entry.getValue()); + } + return new EnvConfigSource(envProperties, ordinal) { @Override public String getName() { return super.getName() + "[source=" + url + "]"; @@ -48,4 +57,9 @@ public static List dotEnvSources(final ClassLoader classLoader) { public static List dotEnvSources(final String location, final ClassLoader classLoader) { return new DotEnvConfigSourceProvider(location).getConfigSources(classLoader); } + + private static String getDotEnvFile(final String filename) { + Path dotEnvFile = Paths.get(System.getProperty("user.dir"), filename); + return Files.isDirectory(dotEnvFile) ? null : dotEnvFile.toUri().toString(); + } } diff --git a/implementation/src/main/java/io/smallrye/config/EnvConfigSource.java b/implementation/src/main/java/io/smallrye/config/EnvConfigSource.java index 715bc2bb7..b701a5035 100644 --- a/implementation/src/main/java/io/smallrye/config/EnvConfigSource.java +++ b/implementation/src/main/java/io/smallrye/config/EnvConfigSource.java @@ -16,62 +16,91 @@ package io.smallrye.config; import static io.smallrye.config.common.utils.ConfigSourceUtil.CONFIG_ORDINAL_KEY; +import static io.smallrye.config.common.utils.StringUtil.isAsciiLetterOrDigit; +import static io.smallrye.config.common.utils.StringUtil.isNumeric; import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores; +import static io.smallrye.config.common.utils.StringUtil.toLowerCaseAndDotted; +import static java.lang.Character.toLowerCase; import static java.security.AccessController.doPrivileged; -import static java.util.Collections.unmodifiableMap; import java.io.Serializable; import java.security.PrivilegedAction; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; -import io.smallrye.config.common.MapBackedConfigSource; +import io.smallrye.config.common.AbstractConfigSource; /** - * @author Jeff Mesnil (c) 2017 Red Hat inc. + * A {@link org.eclipse.microprofile.config.spi.ConfigSource} to access Environment Variables. + *

+ * + * A property name matches to an environment variable with the following rules: + * + *

    + *
  1. Match alphanumeric characters (any case)
  2. + *
  3. Match non-alphanumeric characters with _
  4. + *
  5. Closing quotes in the end of a property name require a double _
  6. + *
+ *

+ * + * Additionally, this implementation provides candidate matching dotted property name from the Environment + * Variable name. These are required when a consumer relies on the list of properties to find additional + * configurations. The MicroProfile Config specification defines a set of conversion rules to look up and find + * values from environment variables even when using their dotted version, but it is unclear about property names. + *
+ * Because an environment variable name may only be represented by a subset of characters, it is not possible + * to represent exactly a dotted version name from an environment variable name, so consumers must be aware of such + * limitations. */ -public class EnvConfigSource extends MapBackedConfigSource { +public class EnvConfigSource extends AbstractConfigSource { private static final long serialVersionUID = -4525015934376795496L; - private static final int DEFAULT_ORDINAL = 300; + public static final String NAME = "EnvConfigSource"; + public static final int ORDINAL = 300; + + private final EnvVars envVars; protected EnvConfigSource() { - this(DEFAULT_ORDINAL); + this(ORDINAL); } protected EnvConfigSource(final int ordinal) { this(getEnvProperties(), ordinal); } - public EnvConfigSource(final Map propertyMap, final int ordinal) { - super("EnvConfigSource", propertyMap, getEnvOrdinal(propertyMap, ordinal)); + public EnvConfigSource(final Map properties, final int ordinal) { + super(NAME, getEnvOrdinal(properties, ordinal)); + this.envVars = new EnvVars(properties); } @Override - public String getValue(final String propertyName) { - return getValue(propertyName, getProperties()); - } - - private static String getValue(final String name, final Map properties) { - if (name == null) { - return null; + public Map getProperties() { + Map properties = new HashMap<>(); + for (Map.Entry entry : envVars.getEnv().entrySet()) { + EnvEntry entryValue = entry.getValue(); + if (entryValue.getEntries() != null) { + properties.putAll(entryValue.getEntries()); + } else { + properties.put(entryValue.getName(), entryValue.getValue()); + } } + return properties; + } - // exact match - String value = properties.get(name); - if (value != null) { - return value; - } + @Override + public Set getPropertyNames() { + return envVars.getNames(); + } - // replace non-alphanumeric characters by underscores - String sanitizedName = replaceNonAlphanumericByUnderscores(name); - value = properties.get(sanitizedName); - if (value != null) { - return value; - } + @Override + public String getValue(final String propertyName) { + return envVars.get(propertyName); + } - // replace non-alphanumeric characters by underscores and convert to uppercase - return properties.get(sanitizedName.toUpperCase()); + boolean hasPropertyName(final String propertyName) { + return envVars.getEnv().containsKey(new EnvName(propertyName)); } /** @@ -80,11 +109,19 @@ private static String getValue(final String name, final Map prop * instantiated in the heap. */ private static Map getEnvProperties() { - return unmodifiableMap(doPrivileged((PrivilegedAction>) () -> new HashMap<>(System.getenv()))); + return doPrivileged(new PrivilegedAction>() { + @Override + public Map run() { + return new HashMap<>(System.getenv()); + } + }); } private static int getEnvOrdinal(final Map properties, final int ordinal) { - final String value = getValue(CONFIG_ORDINAL_KEY, properties); + String value = properties.get(CONFIG_ORDINAL_KEY); + if (value == null) { + value = properties.get(CONFIG_ORDINAL_KEY.toUpperCase()); + } if (value != null) { return Converters.INTEGER_CONVERTER.convert(value); } @@ -102,4 +139,242 @@ Object readResolve() { return new EnvConfigSource(); } } + + static final class EnvVars implements Serializable { + private static final long serialVersionUID = -56318356411229247L; + + private final Map env; + private final Set names; + + public EnvVars(final Map properties) { + this.env = new HashMap<>(properties.size()); + this.names = new HashSet<>(properties.size() * 2); + for (Map.Entry entry : properties.entrySet()) { + EnvName envName = new EnvName(entry.getKey()); + EnvEntry envEntry = env.get(envName); + if (envEntry == null) { + env.put(envName, new EnvEntry(entry.getKey(), entry.getValue())); + } else { + envEntry.add(entry.getKey(), entry.getValue()); + } + this.names.add(entry.getKey()); + this.names.add(toLowerCaseAndDotted(entry.getKey())); + } + } + + public String get(final String propertyName) { + EnvEntry envEntry = env.get(new EnvName(propertyName)); + if (envEntry != null) { + String value = envEntry.get(); + if (value != null) { + return value; + } + + value = envEntry.getEntries().get(propertyName); + if (value != null) { + return value; + } + + String envName = replaceNonAlphanumericByUnderscores(propertyName); + value = envEntry.getEntries().get(envName); + if (value != null) { + return value; + } + + return envEntry.envEntries.get(envName.toUpperCase()); + } + return null; + } + + public Map getEnv() { + return env; + } + + public Set getNames() { + return names; + } + } + + static final class EnvName implements Serializable { + private static final long serialVersionUID = -2679716955093904512L; + + private final String name; + + public EnvName(final String name) { + assert name != null; + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final EnvName that = (EnvName) o; + return equals(this.name, that.name); + } + + @Override + public int hashCode() { + int h = 0; + int length = name.length(); + if (length >= 2) { + if (name.charAt(length - 1) == '_' && name.charAt(length - 2) == '_') { + length = length - 1; + } + } + + for (int i = 0; i < length; i++) { + char c = name.charAt(i); + if (i == 0 && length > 1) { + // The first '%' or '_' is meaninful because it represents a profiled property name + if ((c == '%' || c == '_') && isAsciiLetterOrDigit(name.charAt(i + 1))) { + h = 31 * h + 31; + continue; + } + } + + if (isAsciiLetterOrDigit(c)) { + h = 31 * h + toLowerCase(c); + } + } + return h; + } + + @SuppressWarnings("squid:S4973") + static boolean equals(final String name, final String other) { + //noinspection StringEquality + if (name == other) { + return true; + } + + if (name.isEmpty() && other.isEmpty()) { + return true; + } + + if (name.isEmpty() || other.isEmpty()) { + return false; + } + + char n; + char o; + + int matchPosition = name.length() - 1; + for (int i = other.length() - 1; i >= 0; i--) { + if (matchPosition == -1) { + return false; + } + + o = other.charAt(i); + n = name.charAt(matchPosition); + + // profile + if (i == 0 && (o == '%' || o == '_')) { + if (n == '%' || n == '_') { + return true; + } + } + + if (o == '.') { + if (n != '.' && n != '-' && n != '_' && n != '/') { + return false; + } + } else if (o == '-') { + if (n != '.' && n != '-' && n != '_' && n != '/') { + return false; + } + } else if (o == '"') { + if (n != '"' && n != '_') { + return false; + } else if (n == '_' && name.length() - 1 == matchPosition) { + matchPosition = name.lastIndexOf("_", matchPosition - 1); + if (matchPosition == -1) { + return false; + } + } + } else if (o == ']') { + if (n != ']' && n != '_') { + return false; + } + int beginIndexed = other.lastIndexOf('[', i); + if (beginIndexed != -1) { + int range = i - beginIndexed - 1; + if (name.lastIndexOf('_', matchPosition - 1) == matchPosition - range - 1 + || name.lastIndexOf('[', matchPosition - 1) == matchPosition - range - 1) { + if (isNumeric(other, beginIndexed + range, i) + && isNumeric(name, matchPosition - range, matchPosition)) { + matchPosition = matchPosition - range - 2; + i = i - range - 1; + continue; + } + } + } + return false; + } else if (o == '_') { + if (isAsciiLetterOrDigit(n)) { + return false; + } else if (n == '"' && other.length() - 1 == i) { + i = other.lastIndexOf("_", i - 1); + if (i == -1) { + return false; + } + } + } else if (!isAsciiLetterOrDigit(o)) { + if (o != n && n != '_') { + return false; + + } + } else if (toLowerCase(o) != toLowerCase(n)) { + return false; + } + matchPosition--; + } + + return matchPosition <= 0; + } + } + + static final class EnvEntry implements Serializable { + private static final long serialVersionUID = -8786927401082731020L; + + private final String name; + private final String value; + private Map envEntries; + + EnvEntry(final String name, final String value) { + this.name = name; + this.value = value; + } + + String getName() { + return name; + } + + String getValue() { + return value; + } + + String get() { + return envEntries == null ? value : null; + } + + Map getEntries() { + return envEntries; + } + + void add(String name, String value) { + if (envEntries == null) { + envEntries = new HashMap<>(); + envEntries.put(this.name, this.value); + } + envEntries.put(name, value); + } + } } diff --git a/implementation/src/main/java/io/smallrye/config/ExpressionConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/ExpressionConfigSourceInterceptor.java index 8372d0e07..ec8890e99 100644 --- a/implementation/src/main/java/io/smallrye/config/ExpressionConfigSourceInterceptor.java +++ b/implementation/src/main/java/io/smallrye/config/ExpressionConfigSourceInterceptor.java @@ -1,18 +1,18 @@ package io.smallrye.config; +import static io.smallrye.common.expression.Expression.Flag.DOUBLE_COLON; import static io.smallrye.common.expression.Expression.Flag.LENIENT_SYNTAX; import static io.smallrye.common.expression.Expression.Flag.NO_SMART_BRACES; import static io.smallrye.common.expression.Expression.Flag.NO_TRIM; +import static io.smallrye.config._private.ConfigMessages.msg; -import java.util.Optional; import java.util.function.BiConsumer; -import javax.annotation.Priority; - -import org.eclipse.microprofile.config.Config; +import jakarta.annotation.Priority; import io.smallrye.common.expression.Expression; import io.smallrye.common.expression.ResolveContext; +import io.smallrye.config.ConfigValidationException.Problem; @Priority(Priorities.LIBRARY + 300) public class ExpressionConfigSourceInterceptor implements ConfigSourceInterceptor { @@ -23,14 +23,11 @@ public class ExpressionConfigSourceInterceptor implements ConfigSourceIntercepto private final boolean enabled; public ExpressionConfigSourceInterceptor() { - this.enabled = true; + this(true); } - public ExpressionConfigSourceInterceptor(final ConfigSourceInterceptorContext context) { - this.enabled = Optional.ofNullable(context.proceed(Config.PROPERTY_EXPRESSIONS_ENABLED)) - .map(ConfigValue::getValue) - .map(Boolean::valueOf) - .orElse(Boolean.TRUE); + public ExpressionConfigSourceInterceptor(final boolean enabled) { + this.enabled = enabled; } @Override @@ -39,44 +36,64 @@ public ConfigValue getValue(final ConfigSourceInterceptorContext context, final } private ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name, final int depth) { - if (depth == MAX_DEPTH) { - throw ConfigMessages.msg.expressionExpansionTooDepth(name); + if (depth >= MAX_DEPTH) { + throw msg.expressionExpansionTooDepth(name); } - final ConfigValue configValue = context.proceed(name); + ConfigValue configValue = context.proceed(name); if (!Expressions.isEnabled() || !enabled) { return configValue; } - if (configValue == null) { + if (configValue == null || configValue.getValue() == null) { return null; } - final Expression expression = Expression.compile(escapeDollarIfExists(configValue.getValue()), LENIENT_SYNTAX, NO_TRIM, - NO_SMART_BRACES); - final String expanded = expression.evaluate(new BiConsumer, StringBuilder>() { + // Avoid extra StringBuilder allocations from Expression + if (configValue.getValue().indexOf('$') == -1) { + return configValue; + } + + ConfigValue.ConfigValueBuilder valueBuilder = configValue.from(); + Expression expression = Expression.compile(escapeDollarIfExists(configValue.getValue()), LENIENT_SYNTAX, NO_TRIM, + NO_SMART_BRACES, DOUBLE_COLON); + String expanded = expression.evaluate(new BiConsumer, StringBuilder>() { @Override - public void accept(ResolveContext resolveContext, - StringBuilder stringBuilder) { - final ConfigValue resolve = getValue(context, resolveContext.getKey(), depth + 1); + public void accept(ResolveContext resolveContext, StringBuilder stringBuilder) { + String key = resolveContext.getKey(); + + // Requires a handler lookup + int index = key.indexOf("::"); + if (index != -1) { + valueBuilder.withExtendedExpressionHandler(key.substring(0, index)); + stringBuilder.append(key, index + 2, key.length()); + return; + } + + // Expression lookup + ConfigValue resolve = getValue(context, key, depth + 1); if (resolve != null) { - stringBuilder.append(resolve.getValue()); + if (!resolve.hasProblems()) { + stringBuilder.append(resolve.getValue()); + } else { + valueBuilder.withProblems(resolve.getProblems()); + } } else if (resolveContext.hasDefault()) { resolveContext.expandDefault(); } else { - throw ConfigMessages.msg.expandingElementNotFound(resolveContext.getKey(), configValue.getName()); + valueBuilder.addProblem(new Problem(msg.expandingElementNotFound(key, configValue.getName()))); } } }); - return configValue.withValue(expanded); + return valueBuilder.withValue(expanded).build(); } /** * MicroProfile Config defines the backslash escape for dollar to retrieve the raw expression. We don't want to * turn {@link Expression.Flag#ESCAPES} on because it may break working configurations. - * + *
* This will replace the expected escape in MicroProfile Config by the escape used in {@link Expression}, a double * dollar. */ diff --git a/implementation/src/main/java/io/smallrye/config/FallbackConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/FallbackConfigSourceInterceptor.java index 5f6241755..5c6babb6d 100644 --- a/implementation/src/main/java/io/smallrye/config/FallbackConfigSourceInterceptor.java +++ b/implementation/src/main/java/io/smallrye/config/FallbackConfigSourceInterceptor.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.function.Function; -import javax.annotation.Priority; +import jakarta.annotation.Priority; @Priority(Priorities.LIBRARY + 600) public class FallbackConfigSourceInterceptor extends AbstractMappingConfigSourceInterceptor { @@ -31,9 +31,15 @@ public ConfigValue getValue(final ConfigSourceInterceptorContext context, final ConfigValue fallbackValue = context.proceed(map); // Check which one comes from a higher ordinal source if (configValue != null && fallbackValue != null) { - return CONFIG_SOURCE_COMPARATOR.compare(configValue, fallbackValue) >= 0 ? configValue : fallbackValue; + return CONFIG_SOURCE_COMPARATOR.compare(configValue, fallbackValue) >= 0 ? configValue + : fallbackValue.withName(name); } else { - return configValue != null ? configValue : fallbackValue; + if (configValue != null) { + return configValue; + } else if (fallbackValue != null) { + return fallbackValue.withName(name); + } + return null; } } } diff --git a/implementation/src/main/java/io/smallrye/config/ImplicitConverters.java b/implementation/src/main/java/io/smallrye/config/ImplicitConverters.java index 8a189700d..a3fc35339 100644 --- a/implementation/src/main/java/io/smallrye/config/ImplicitConverters.java +++ b/implementation/src/main/java/io/smallrye/config/ImplicitConverters.java @@ -27,6 +27,7 @@ import org.eclipse.microprofile.config.spi.Converter; +import io.smallrye.config._private.ConfigMessages; import io.smallrye.config.common.utils.StringUtil; /** @@ -229,12 +230,11 @@ public E convert(final String value) throws IllegalArgumentException, NullPointe return enumType.cast(enumValue); } - throw new IllegalArgumentException(String.format("Cannot convert %s to enum %s", value, enumType)); + throw ConfigMessages.msg.cannotConvertEnum(value, enumType, String.join(",", values.keySet())); } private static String hyphenate(String value) { - // We cannot be sure about the format of the enum names - return StringUtil.skewer(value).replaceAll("_-", "-").replaceAll("_", "-"); + return StringUtil.skewer(value); } } } diff --git a/implementation/src/main/java/io/smallrye/config/KeyMap.java b/implementation/src/main/java/io/smallrye/config/KeyMap.java index 8d0b8ebab..20c493bf0 100644 --- a/implementation/src/main/java/io/smallrye/config/KeyMap.java +++ b/implementation/src/main/java/io/smallrye/config/KeyMap.java @@ -2,6 +2,7 @@ import java.io.Serializable; import java.util.HashMap; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.Map; @@ -110,13 +111,25 @@ public KeyMap find(final String path) { } public KeyMap find(final NameIterator ni) { + return find(ni, new StringBuilder()); + } + + KeyMap find(final NameIterator ni, final StringBuilder sb) { if (!ni.hasNext()) { return this; } - String seg = ni.getNextSegment(); - ni.next(); + String seg = ni.getNextSegment(sb).toString(); + if (seg.isEmpty()) { + return null; + } KeyMap next = findOrDefault(seg); - return next == null ? null : next.find(ni); + if (next != null) { + ni.next(); + sb.setLength(0); + return next.find(ni, sb); + } else { + return null; + } } public KeyMap find(final Iterator iter) { @@ -124,6 +137,9 @@ public KeyMap find(final Iterator iter) { return this; } String seg = iter.next(); + if (seg.isEmpty()) { + return null; + } KeyMap next = seg.equals("*") ? any : getOrDefault(seg, any); return next == null ? null : next.find(iter); } @@ -169,15 +185,20 @@ public KeyMap findOrAdd(final String path) { return findOrAdd(new NameIterator(path)); } - public KeyMap findOrAdd(final NameIterator ni) { + public KeyMap findOrAdd(final NameIterator path) { + return findOrAdd(path, new StringBuilder()); + } + + KeyMap findOrAdd(final NameIterator ni, final StringBuilder sb) { if (!ni.hasNext()) { return this; } - String seg = ni.getNextSegment(); + String seg = ni.getNextSegment(sb).toString(); ni.next(); try { KeyMap next = getNext(seg); - return next.findOrAdd(ni); + sb.setLength(0); + return next.findOrAdd(ni, sb); } finally { ni.previous(); } @@ -211,7 +232,7 @@ private KeyMap getNext(final String seg) { return getOrCreateAny(); } else if (seg.endsWith("]")) { int begin = seg.lastIndexOf('['); - if (begin != -1) { + if (begin != -1 && isValidIndex(seg, begin)) { String index = seg.substring(begin + 1, seg.length() - 1); String name = seg.substring(0, begin); KeyMap next; @@ -237,7 +258,8 @@ public V findRootValue(final String path) { } public V findRootValue(final NameIterator ni) { - KeyMap result = find(ni); + StringBuilder sb = new StringBuilder(); + KeyMap result = find(ni, sb); return result == null ? null : result.getRootValue(); } @@ -310,6 +332,29 @@ public void putAll(final Map> m) { } } + @Override + public Set keySet() { + Set keys = super.keySet(); + Set allKeys = new HashSet<>(); + for (String key : keys) { + KeyMap childMap = find(key); + if (childMap != null) { + Set childKeys = childMap.keySet(); + for (String childKey : childKeys) { + if (key.endsWith("[") || childKey.startsWith("[") || childKey.startsWith("]")) { + allKeys.add(key + childKey); + } else { + allKeys.add(key + "." + childKey); + } + } + } + if (hasRootValue(key)) { + allKeys.add(key); + } + } + return allKeys; + } + @SuppressWarnings("ResultOfMethodCallIgnored") public StringBuilder toString(StringBuilder b) { b.append("KeyMap("); diff --git a/implementation/src/main/java/io/smallrye/config/KeyMapBackedConfigSource.java b/implementation/src/main/java/io/smallrye/config/KeyMapBackedConfigSource.java index 08e644444..ecc6c87d6 100644 --- a/implementation/src/main/java/io/smallrye/config/KeyMapBackedConfigSource.java +++ b/implementation/src/main/java/io/smallrye/config/KeyMapBackedConfigSource.java @@ -8,6 +8,7 @@ import io.smallrye.config.common.AbstractConfigSource; +@Deprecated(forRemoval = true) public class KeyMapBackedConfigSource extends AbstractConfigSource { private static final long serialVersionUID = 4378754290346888762L; @@ -34,7 +35,7 @@ KeyMap getKeyMapProperties() { @Override public Set getPropertyNames() { - return Collections.emptySet(); + return properties.keySet(); } @Override diff --git a/implementation/src/main/java/io/smallrye/config/LoggingConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/LoggingConfigSourceInterceptor.java index 22998348a..5da68e9e9 100644 --- a/implementation/src/main/java/io/smallrye/config/LoggingConfigSourceInterceptor.java +++ b/implementation/src/main/java/io/smallrye/config/LoggingConfigSourceInterceptor.java @@ -2,21 +2,38 @@ import static io.smallrye.config.SecretKeys.doLocked; -import javax.annotation.Priority; +import jakarta.annotation.Priority; -@Priority(Priorities.LIBRARY + 150) +import io.smallrye.config._private.ConfigLogging; + +@Priority(Priorities.LIBRARY + 250) public class LoggingConfigSourceInterceptor implements ConfigSourceInterceptor { private static final long serialVersionUID = 367246512037404779L; + private final boolean enabled; + + public LoggingConfigSourceInterceptor() { + this(true); + } + + public LoggingConfigSourceInterceptor(final boolean enabled) { + this.enabled = enabled; + } + @Override public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + if (!enabled || !ConfigLogging.log.isDebugEnabled()) { + return context.proceed(name); + } + try { // Unlocked keys will run here. ConfigValue configValue = doLocked(() -> context.proceed(name)); - if (configValue != null) + if (configValue != null) { ConfigLogging.log.lookup(configValue.getName(), configValue.getLocation(), configValue.getValue()); - else + } else { ConfigLogging.log.notFound(name); + } return configValue; } catch (SecurityException e) { // Handled next, to omit the values to log from the secret. @@ -24,10 +41,11 @@ public ConfigValue getValue(final ConfigSourceInterceptorContext context, final // Locked keys here. final ConfigValue secret = context.proceed(name); - if (secret != null) + if (secret != null) { ConfigLogging.log.lookup(secret.getName(), "secret", "secret"); - else + } else { ConfigLogging.log.notFound(name); + } return secret; } } diff --git a/implementation/src/main/java/io/smallrye/config/MapBackedConfigValueConfigSource.java b/implementation/src/main/java/io/smallrye/config/MapBackedConfigValueConfigSource.java index 34c954949..ff2984b21 100644 --- a/implementation/src/main/java/io/smallrye/config/MapBackedConfigValueConfigSource.java +++ b/implementation/src/main/java/io/smallrye/config/MapBackedConfigValueConfigSource.java @@ -10,6 +10,7 @@ import io.smallrye.config.common.AbstractConfigSource; import io.smallrye.config.common.utils.ConfigSourceUtil; +@Deprecated(forRemoval = true) public abstract class MapBackedConfigValueConfigSource extends AbstractConfigSource implements ConfigValueConfigSource { private static final long serialVersionUID = -4619155951589529987L; diff --git a/implementation/src/main/java/io/smallrye/config/NameIterator.java b/implementation/src/main/java/io/smallrye/config/NameIterator.java index 5c450857b..245905967 100644 --- a/implementation/src/main/java/io/smallrye/config/NameIterator.java +++ b/implementation/src/main/java/io/smallrye/config/NameIterator.java @@ -94,15 +94,15 @@ private int initIteration() { return this.pos & POS_MASK; } - private int cookieOf(int state, int pos) { + private static int cookieOf(int state, int pos) { return state << POS_BITS | pos & POS_MASK; } - private int getPosition(int cookie) { + private static int getPosition(int cookie) { return (cookie & POS_MASK) << SE_SHIFT >> SE_SHIFT; } - private int getState(int cookie) { + private static int getState(int cookie) { return cookie >> POS_BITS; } @@ -253,12 +253,15 @@ public boolean nextSegmentEquals(String other, int offs, int len) { } public String getNextSegment() { - final StringBuilder b = new StringBuilder(); + return getNextSegment(new StringBuilder()).toString(); + } + + public StringBuilder getNextSegment(StringBuilder b) { int cookie = initIteration(); for (;;) { cookie = nextPos(cookie); if (isSegmentDelimiter(cookie)) { - return b.toString(); + return b; } b.append((char) charAt(cookie)); } @@ -326,6 +329,12 @@ public void next() { pos = getNextEnd(); } + public void next(int segments) { + for (int i = 0; i < segments; i++) { + next(); + } + } + public void previous() { pos = getPreviousStart() - 1; } diff --git a/implementation/src/main/java/io/smallrye/config/Priorities.java b/implementation/src/main/java/io/smallrye/config/Priorities.java index bf095c53a..60717098e 100644 --- a/implementation/src/main/java/io/smallrye/config/Priorities.java +++ b/implementation/src/main/java/io/smallrye/config/Priorities.java @@ -1,12 +1,9 @@ package io.smallrye.config; -import io.smallrye.common.annotation.Experimental; - /** * A collection of built-in priority constants for {@link ConfigSourceInterceptor} that are supposed to be - * ordered based on their {@code javax.annotation.Priority} class-level annotation. + * ordered based on their {@code jakarta.annotation.Priority} class-level annotation. */ -@Experimental("Interceptor API to intercept resolution of a configuration name") public final class Priorities { /** * Range for early interceptors defined by Platform specifications. diff --git a/implementation/src/main/java/io/smallrye/config/ProfileConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/ProfileConfigSourceInterceptor.java index 1b4501acb..cc2f04e07 100644 --- a/implementation/src/main/java/io/smallrye/config/ProfileConfigSourceInterceptor.java +++ b/implementation/src/main/java/io/smallrye/config/ProfileConfigSourceInterceptor.java @@ -4,18 +4,15 @@ import static io.smallrye.config.Converters.STRING_CONVERTER; import static io.smallrye.config.Converters.newCollectionConverter; import static io.smallrye.config.Converters.newTrimmingConverter; -import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_PROFILE; -import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_PROFILE_PARENT; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.NoSuchElementException; import java.util.Set; -import javax.annotation.Priority; +import jakarta.annotation.Priority; @Priority(Priorities.LIBRARY + 200) public class ProfileConfigSourceInterceptor implements ConfigSourceInterceptor { @@ -32,23 +29,15 @@ public ProfileConfigSourceInterceptor(final List profiles) { this.profiles = reverseProfiles.toArray(new String[0]); } - public ProfileConfigSourceInterceptor(final ConfigSourceInterceptorContext context) { - this(getProfile(context)); - } - @Override public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { if (profiles.length > 0) { - final String normalizeName = normalizeName(name); + final String normalizeName = activeName(name, profiles); final ConfigValue profileValue = getProfileValue(context, normalizeName); if (profileValue != null) { - try { - final ConfigValue originalValue = context.proceed(normalizeName); - if (originalValue != null && CONFIG_SOURCE_COMPARATOR.compare(originalValue, profileValue) > 0) { - return originalValue; - } - } catch (final NoSuchElementException e) { - // We couldn't find the main property so we fallback to the profile property because it exists. + final ConfigValue originalValue = context.proceed(normalizeName); + if (originalValue != null && CONFIG_SOURCE_COMPARATOR.compare(originalValue, profileValue) > 0) { + return originalValue; } return profileValue.withName(normalizeName); } @@ -73,33 +62,62 @@ public Iterator iterateNames(final ConfigSourceInterceptorContext contex final Set names = new HashSet<>(); final Iterator namesIterator = context.iterateNames(); while (namesIterator.hasNext()) { - names.add(normalizeName(namesIterator.next())); + names.add(activeName(namesIterator.next(), profiles)); } return names.iterator(); } - @Override - public Iterator iterateValues(final ConfigSourceInterceptorContext context) { - final Set values = new HashSet<>(); - final Iterator valuesIterator = context.iterateValues(); - while (valuesIterator.hasNext()) { - final ConfigValue value = valuesIterator.next(); - values.add(value.withName(normalizeName(value.getName()))); - } - return values.iterator(); - } - public String[] getProfiles() { return profiles; } - private String normalizeName(final String name) { - for (String profile : profiles) { - if (name.startsWith("%" + profile + ".")) { - return name.substring(profile.length() + 2); + public static String activeName(final String name, final String[] profiles) { + if (!name.isEmpty() && name.charAt(0) == '%') { + int profilesEnd = name.indexOf('.', 1); + int multipleSplit = -1; + for (int i = 1; i < profilesEnd; i++) { + if (name.charAt(i) == ',') { + multipleSplit = i; + break; + } } - } + if (multipleSplit == -1) { + // Single profile property name (%profile.foo.bar) + for (String profile : profiles) { + if (profilesEnd == profile.length() + 1 && name.regionMatches(1, profile, 0, profile.length())) { + return name.substring(profilesEnd + 1); + } + } + } else { + // Multiple profile property name (%profile,another.foo.bar) + int nextSplit = multipleSplit; + int toOffset = 1; + while (nextSplit != -1) { + for (String profile : profiles) { + char expectedEnd = name.charAt(toOffset + profile.length()); + if ((expectedEnd == '.' || expectedEnd == ',') && + name.regionMatches(toOffset, profile, 0, profile.length())) { + return name.substring(profilesEnd + 1); + } + } + + toOffset = nextSplit + 1; + nextSplit = -1; + + for (int i = toOffset; i < profilesEnd; i++) { + if (name.charAt(i) == ',') { + nextSplit = i; + break; + } + } + + if (toOffset < profilesEnd && nextSplit == -1) { + nextSplit = profilesEnd; + } + } + } + } return name; } @@ -107,24 +125,4 @@ public static List convertProfile(final String profile) { List profiles = newCollectionConverter(newTrimmingConverter(STRING_CONVERTER), ArrayList::new).convert(profile); return profiles != null ? profiles : Collections.emptyList(); } - - private static List getProfile(final ConfigSourceInterceptorContext context) { - final List profiles = new ArrayList<>(); - profiles.addAll(getProfiles(context, SMALLRYE_CONFIG_PROFILE_PARENT)); - profiles.addAll(getProfiles(context, SMALLRYE_CONFIG_PROFILE)); - return profiles; - } - - private static List getProfiles(final ConfigSourceInterceptorContext context, final String propertyName) { - final List profiles = new ArrayList<>(); - final ConfigValue profileValue = context.proceed(propertyName); - if (profileValue != null) { - final List convertProfiles = convertProfile(profileValue.getValue()); - for (String profile : convertProfiles) { - profiles.addAll(getProfiles(context, "%" + profile + "." + SMALLRYE_CONFIG_PROFILE_PARENT)); - profiles.add(profile); - } - } - return profiles; - } } diff --git a/implementation/src/main/java/io/smallrye/config/PropertiesConfigSourceProvider.java b/implementation/src/main/java/io/smallrye/config/PropertiesConfigSourceProvider.java index 9c86ff0a3..5ba46d0e3 100644 --- a/implementation/src/main/java/io/smallrye/config/PropertiesConfigSourceProvider.java +++ b/implementation/src/main/java/io/smallrye/config/PropertiesConfigSourceProvider.java @@ -37,14 +37,6 @@ public PropertiesConfigSourceProvider(final String location, final ClassLoader c this.configSources.addAll(loadConfigSources(location, ConfigSource.DEFAULT_ORDINAL, classLoader)); } - @Deprecated - public PropertiesConfigSourceProvider(String location, boolean optional, ClassLoader classLoader) { - this(location, classLoader, false); - if (!optional && this.configSources.isEmpty()) { - throw ConfigMessages.msg.fileNotFound(location); - } - } - @Override public List getConfigSources(ClassLoader forClassLoader) { return configSources; diff --git a/implementation/src/main/java/io/smallrye/config/PropertyName.java b/implementation/src/main/java/io/smallrye/config/PropertyName.java new file mode 100644 index 000000000..c1973a509 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/PropertyName.java @@ -0,0 +1,108 @@ +package io.smallrye.config; + +import static io.smallrye.config.common.utils.StringUtil.isNumeric; + +public class PropertyName { + private final String name; + + public PropertyName(final String name) { + this.name = name; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final PropertyName that = (PropertyName) o; + return equals(this.name, that.name) || equals(that.name, this.name); + } + + @SuppressWarnings("squid:S4973") + static boolean equals(final String name, final String other) { + //noinspection StringEquality + if (name == other) { + return true; + } + + char n; + char o; + + int matchPosition = name.length() - 1; + for (int i = other.length() - 1; i >= 0; i--) { + if (matchPosition == -1) { + return false; + } + + o = other.charAt(i); + n = name.charAt(matchPosition); + + if (n == '*') { + if (o == ']') { + return false; + } else if (o == '"') { + int beginQuote = other.lastIndexOf('"', i - 1); + if (beginQuote != -1 && beginQuote != 0 && other.charAt(beginQuote - 1) == '.') { + i = beginQuote; + } + } else { + int previousDot = other.lastIndexOf('.', i); + if (previousDot != -1) { + i = previousDot + 1; + } else { + i = 0; + } + } + } else if (n == ']' && o == ']') { + if (name.length() >= 3 && other.length() >= 3 + && name.charAt(matchPosition - 1) == '*' && name.charAt(matchPosition - 2) == '[' + && other.charAt(i - 1) == '*' && other.charAt(i - 2) == '[') { + matchPosition = matchPosition - 2; + i = i - 1; + continue; + } else { + int beginIndexed = other.lastIndexOf('[', i); + if (beginIndexed != -1) { + int range = i - beginIndexed - 1; + if (isNumeric(other, beginIndexed + range, i)) { + matchPosition = matchPosition - 3; + i = i - range - 1; + continue; + } + } + } + return false; + } else if (o != n) { + return false; + } + matchPosition--; + } + return matchPosition <= 0; + } + + @Override + public int hashCode() { + int h = 0; + int length = name.length(); + boolean quotesOpen = false; + for (int i = 0; i < length; i++) { + char c = name.charAt(i); + if (quotesOpen) { + if (c == '"') { + quotesOpen = false; + } + continue; + } else if (c == '"') { + quotesOpen = true; + continue; + } else if (c != '.' && c != '[' && c != ']') { + continue; + } + h = 31 * h + c; + } + return h; + } +} diff --git a/implementation/src/main/java/io/smallrye/config/PropertyNamesConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/PropertyNamesConfigSourceInterceptor.java deleted file mode 100644 index 3e58d24c2..000000000 --- a/implementation/src/main/java/io/smallrye/config/PropertyNamesConfigSourceInterceptor.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.smallrye.config; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; - -/** - * This interceptor adds additional entries to {@link org.eclipse.microprofile.config.Config#getPropertyNames}. - */ -class PropertyNamesConfigSourceInterceptor implements ConfigSourceInterceptor { - private static final long serialVersionUID = 5263983885197566053L; - - private final Set properties = new HashSet<>(); - - @Override - public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { - return context.proceed(name); - } - - @Override - public Iterator iterateNames(final ConfigSourceInterceptorContext context) { - final Set names = new HashSet<>(); - final Iterator namesIterator = context.iterateNames(); - while (namesIterator.hasNext()) { - names.add(namesIterator.next()); - } - names.addAll(properties); - return names.iterator(); - } - - void addProperties(final Set properties) { - this.properties.addAll(properties); - } -} diff --git a/implementation/src/main/java/io/smallrye/config/RelocateConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/RelocateConfigSourceInterceptor.java index 72711a986..2c5927dd6 100644 --- a/implementation/src/main/java/io/smallrye/config/RelocateConfigSourceInterceptor.java +++ b/implementation/src/main/java/io/smallrye/config/RelocateConfigSourceInterceptor.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.function.Function; -import javax.annotation.Priority; +import jakarta.annotation.Priority; @Priority(Priorities.LIBRARY + 300) public class RelocateConfigSourceInterceptor extends AbstractMappingConfigSourceInterceptor { @@ -31,9 +31,15 @@ public ConfigValue getValue(final ConfigSourceInterceptorContext context, final ConfigValue configValue = context.proceed(name); // Check which one comes from a higher ordinal source if (relocateValue != null && configValue != null) { - return CONFIG_SOURCE_COMPARATOR.compare(relocateValue, configValue) >= 0 ? relocateValue : configValue; + return CONFIG_SOURCE_COMPARATOR.compare(relocateValue, configValue) >= 0 ? relocateValue + : configValue.withName(map); } else { - return relocateValue != null ? relocateValue : configValue; + if (relocateValue != null) { + return relocateValue; + } else if (configValue != null) { + return configValue.withName(map); + } + return null; } } } diff --git a/implementation/src/main/java/io/smallrye/config/SecretKeys.java b/implementation/src/main/java/io/smallrye/config/SecretKeys.java index 7ac20cdbb..be0371312 100644 --- a/implementation/src/main/java/io/smallrye/config/SecretKeys.java +++ b/implementation/src/main/java/io/smallrye/config/SecretKeys.java @@ -1,18 +1,17 @@ package io.smallrye.config; +import java.io.Serializable; import java.util.function.Supplier; @SuppressWarnings("squid:S5164") -public final class SecretKeys { - private static final ThreadLocal LOCKED = new ThreadLocal<>(); +public final class SecretKeys implements Serializable { + private static final long serialVersionUID = -3226034787747746735L; - private SecretKeys() { - throw new UnsupportedOperationException(); - } + private static final ThreadLocal LOCKED = new ThreadLocal<>(); public static boolean isLocked() { Boolean result = LOCKED.get(); - return result == null ? true : result; + return result == null || result; } public static void doUnlocked(Runnable runnable) { diff --git a/implementation/src/main/java/io/smallrye/config/SecretKeysConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/SecretKeysConfigSourceInterceptor.java index 920a3fda8..b2e2e69db 100644 --- a/implementation/src/main/java/io/smallrye/config/SecretKeysConfigSourceInterceptor.java +++ b/implementation/src/main/java/io/smallrye/config/SecretKeysConfigSourceInterceptor.java @@ -4,7 +4,9 @@ import java.util.Iterator; import java.util.Set; -import javax.annotation.Priority; +import jakarta.annotation.Priority; + +import io.smallrye.config._private.ConfigMessages; @Priority(Priorities.LIBRARY + 100) public class SecretKeysConfigSourceInterceptor implements ConfigSourceInterceptor { @@ -18,7 +20,7 @@ public SecretKeysConfigSourceInterceptor(final Set secrets) { @Override public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { - if (SecretKeys.isLocked() && isSecret(name)) { + if (SecretKeys.isLocked() && secrets.contains(name)) { throw ConfigMessages.msg.notAllowed(name); } return context.proceed(name); @@ -39,24 +41,4 @@ public Iterator iterateNames(final ConfigSourceInterceptorContext contex } return context.iterateNames(); } - - @Override - public Iterator iterateValues(final ConfigSourceInterceptorContext context) { - if (SecretKeys.isLocked()) { - Set values = new HashSet<>(); - Iterator valuesIterator = context.iterateValues(); - while (valuesIterator.hasNext()) { - ConfigValue value = valuesIterator.next(); - if (!secrets.contains(value.getName())) { - values.add(value); - } - } - return values.iterator(); - } - return context.iterateValues(); - } - - private boolean isSecret(final String name) { - return secrets.contains(name); - } } diff --git a/implementation/src/main/java/io/smallrye/config/SecretKeysHandler.java b/implementation/src/main/java/io/smallrye/config/SecretKeysHandler.java new file mode 100644 index 000000000..59b7fc57d --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/SecretKeysHandler.java @@ -0,0 +1,10 @@ +package io.smallrye.config; + +import io.smallrye.common.annotation.Experimental; + +@Experimental("") +public interface SecretKeysHandler { + String decode(String secret); + + String getName(); +} diff --git a/implementation/src/main/java/io/smallrye/config/SecretKeysHandlerConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/SecretKeysHandlerConfigSourceInterceptor.java new file mode 100644 index 000000000..645af200f --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/SecretKeysHandlerConfigSourceInterceptor.java @@ -0,0 +1,57 @@ +package io.smallrye.config; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import io.smallrye.config.SecretKeysHandlerFactory.LazySecretKeysHandler; +import io.smallrye.config._private.ConfigMessages; + +public class SecretKeysHandlerConfigSourceInterceptor implements ConfigSourceInterceptor { + private static final long serialVersionUID = -5228028387733656005L; + + private final boolean enabled; + private final Map handlers = new HashMap<>(); + + public SecretKeysHandlerConfigSourceInterceptor(final boolean enabled, final List handlers) { + this.enabled = enabled; + for (SecretKeysHandler handler : handlers) { + this.handlers.put(handler.getName(), handler); + } + } + + @Override + public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + ConfigValue configValue = context.proceed(name); + if (enabled && configValue != null && configValue.getValue() != null) { + String handler = configValue.getExtendedExpressionHandler(); + if (handler != null) { + return configValue.withValue(getHandler(handler, context).decode(configValue.getValue())); + } + } + return configValue; + } + + private SecretKeysHandler getHandler(final String handlerName, final ConfigSourceInterceptorContext context) { + SecretKeysHandler handler = handlers.get(handlerName); + if (handler == null) { + throw ConfigMessages.msg.secretKeyHandlerNotFound(handlerName); + } + + if (handler instanceof LazySecretKeysHandler) { + handler = ((LazySecretKeysHandler) handler).get(new ConfigSourceContext() { + @Override + public ConfigValue getValue(final String name) { + return context.proceed(name); + } + + @Override + public Iterator iterateNames() { + return context.iterateNames(); + } + }); + } + return handler; + } +} diff --git a/implementation/src/main/java/io/smallrye/config/SecretKeysHandlerFactory.java b/implementation/src/main/java/io/smallrye/config/SecretKeysHandlerFactory.java new file mode 100644 index 000000000..176154847 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/SecretKeysHandlerFactory.java @@ -0,0 +1,41 @@ +package io.smallrye.config; + +import java.util.concurrent.atomic.AtomicReference; + +import io.smallrye.common.annotation.Experimental; + +@Experimental("") +public interface SecretKeysHandlerFactory { + SecretKeysHandler getSecretKeysHandler(ConfigSourceContext context); + + String getName(); + + class LazySecretKeysHandler implements SecretKeysHandler { + private final SecretKeysHandlerFactory factory; + private final AtomicReference handler = new AtomicReference<>(); + + public LazySecretKeysHandler(final SecretKeysHandlerFactory factory) { + this.factory = factory; + } + + public SecretKeysHandler get(ConfigSourceContext configSourceContext) { + if (handler.get() == null) { + handler.compareAndSet(null, factory.getSecretKeysHandler(configSourceContext)); + } + return handler.get(); + } + + @Override + public String decode(final String secret) { + if (handler.get() == null) { + throw new IllegalStateException(); + } + return handler.get().decode(secret); + } + + @Override + public String getName() { + return factory.getName(); + } + } +} diff --git a/implementation/src/main/java/io/smallrye/config/SecuritySupport.java b/implementation/src/main/java/io/smallrye/config/SecuritySupport.java index 1595f4ad8..92974214d 100644 --- a/implementation/src/main/java/io/smallrye/config/SecuritySupport.java +++ b/implementation/src/main/java/io/smallrye/config/SecuritySupport.java @@ -24,6 +24,8 @@ import java.security.PrivilegedExceptionAction; import java.util.Arrays; +import io.smallrye.config._private.ConfigLogging; + /** * @author Jeff Mesnil (c) 2018 Red Hat inc. */ diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java index 82ec9a0ef..428274e11 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java @@ -15,9 +15,14 @@ */ package io.smallrye.config; +import static io.smallrye.config.ConfigMappingLoader.getConfigMappingClass; import static io.smallrye.config.ConfigSourceInterceptor.EMPTY; -import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores; -import static io.smallrye.config.common.utils.StringUtil.toLowerCaseAndDotted; +import static io.smallrye.config.Converters.newCollectionConverter; +import static io.smallrye.config.Converters.newMapConverter; +import static io.smallrye.config.Converters.newOptionalConverter; +import static io.smallrye.config.common.utils.StringUtil.unindexed; +import static io.smallrye.config.common.utils.StringUtil.unquoted; +import static java.util.stream.Collectors.toList; import java.io.ObjectStreamException; import java.io.Serializable; @@ -33,7 +38,6 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -41,11 +45,16 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.inject.ConfigProperties; import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.ConfigSourceProvider; import org.eclipse.microprofile.config.spi.Converter; import io.smallrye.common.annotation.Experimental; import io.smallrye.config.SmallRyeConfigBuilder.InterceptorWithPriority; +import io.smallrye.config._private.ConfigLogging; +import io.smallrye.config._private.ConfigMessages; +import io.smallrye.config.common.utils.StringUtil; /** * @author Jeff Mesnil (c) 2017 Red Hat inc. @@ -55,6 +64,7 @@ public class SmallRyeConfig implements Config, Serializable { public static final String SMALLRYE_CONFIG_PROFILE_PARENT = "smallrye.config.profile.parent"; public static final String SMALLRYE_CONFIG_LOCATIONS = "smallrye.config.locations"; public static final String SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN = "smallrye.config.mapping.validate-unknown"; + public static final String SMALLRYE_CONFIG_LOG_VALUES = "smallrye.config.log.values"; private static final long serialVersionUID = 8138651532357898263L; @@ -62,12 +72,16 @@ public class SmallRyeConfig implements Config, Serializable { private final Map> converters; private final Map>> optionalConverters = new ConcurrentHashMap<>(); - private final ConfigMappings mappings; + private final ConfigValidator configValidator; + private final Map, Map> mappings; - SmallRyeConfig(SmallRyeConfigBuilder builder, ConfigMappings mappings) { - this.configSources = new ConfigSources(builder); + SmallRyeConfig(SmallRyeConfigBuilder builder) { + // This needs to be executed before everything else to make sure that defaults from mappings are available to all sources + ConfigMappingProvider mappingProvider = builder.getMappingsBuilder().build(); + this.configSources = new ConfigSources(builder, this); this.converters = buildConverters(builder); - this.mappings = mappings; + this.configValidator = builder.getValidator(); + this.mappings = new ConcurrentHashMap<>(mappingProvider.mapConfiguration(this)); } private Map> buildConverters(final SmallRyeConfigBuilder builder) { @@ -87,14 +101,14 @@ private Map> buildConverters(final SmallRyeConfigBuilder buil for (Map.Entry entry : convertersToBuild.entrySet()) { converters.put(entry.getKey(), entry.getValue().getConverter()); } - converters.put(ConfigValue.class, ConfigValueConverter.CONFIG_VALUE_CONVERTER); + converters.put(ConfigValue.class, Converters.CONFIG_VALUE_CONVERTER); return converters; } @Override - public List getValues(final String propertyName, final Class propertyType) { - return getValues(propertyName, propertyType, ArrayList::new); + public List getValues(final String name, final Class propertyType) { + return getValues(name, propertyType, ArrayList::new); } public > C getValues(String name, Class itemClass, IntFunction collectionFactory) { @@ -103,7 +117,7 @@ public > C getValues(String name, Class itemClass, public > C getValues(String name, Converter converter, IntFunction collectionFactory) { try { - return getValue(name, Converters.newCollectionConverter(converter, collectionFactory)); + return getValue(name, newCollectionConverter(converter, collectionFactory)); } catch (NoSuchElementException e) { return getIndexedValues(name, converter, collectionFactory); } @@ -125,12 +139,20 @@ public > C getIndexedValues(String name, Converter } public List getIndexedProperties(final String property) { - List indexes = getIndexedPropertiesIndexes(property); List indexedProperties = new ArrayList<>(); - for (Integer index : indexes) { - indexedProperties.add(property + "[" + index + "]"); + for (String propertyName : this.getPropertyNames()) { + if (propertyName.startsWith(property) && propertyName.length() > property.length()) { + int indexStart = property.length(); + if (propertyName.charAt(indexStart) == '[') { + int indexEnd = propertyName.indexOf(']', indexStart); + if (indexEnd != -1 && propertyName.charAt(propertyName.length() - 1) != '.' + && StringUtil.isNumeric(propertyName, indexStart + 1, indexEnd)) { + indexedProperties.add(propertyName); + } + } + } } - + Collections.sort(indexedProperties); return indexedProperties; } @@ -138,21 +160,12 @@ public List getIndexedPropertiesIndexes(final String property) { Set indexes = new HashSet<>(); for (String propertyName : this.getPropertyNames()) { if (propertyName.startsWith(property) && propertyName.length() > property.length()) { - int index = property.length(); - if (propertyName.charAt(index) == '[') { - for (;;) { - if (propertyName.charAt(index) == ']') { - try { - indexes.add(Integer.parseInt(propertyName.substring(property.length() + 1, index))); - } catch (NumberFormatException e) { - //NOOP - } - break; - } else if (index < propertyName.length() - 1) { - index++; - } else { - break; - } + int indexStart = property.length(); + if (propertyName.charAt(indexStart) == '[') { + int indexEnd = propertyName.indexOf(']', indexStart); + if (indexEnd != -1 && propertyName.charAt(propertyName.length() - 1) != '.' + && StringUtil.isNumeric(propertyName, indexStart + 1, indexEnd)) { + indexes.add(Integer.parseInt(propertyName.substring(indexStart + 1, indexEnd))); } } } @@ -162,30 +175,24 @@ public List getIndexedPropertiesIndexes(final String property) { return sortIndexes; } - @Override - public T getValue(String name, Class aClass) { - return getValue(name, requireConverter(aClass)); - } - /** * Return the content of the direct sub properties as the requested type of Map. * * @param name The configuration property name - * @param kClass the type into which the keys should be converted - * @param vClass the type into which the values should be converted + * @param keyClass the type into which the keys should be converted + * @param valueClass the type into which the values should be converted * @param the key type * @param the value type * @return the resolved property value as an instance of the requested Map (not {@code null}) * @throws IllegalArgumentException if a key or a value cannot be converted to the specified types * @throws NoSuchElementException if no direct sub properties could be found. */ - @Experimental("Extension to retrieve mandatory sub properties as a Map") - public Map getValues(String name, Class kClass, Class vClass) { - final Map result = getValuesAsMap(name, requireConverter(kClass), requireConverter(vClass)); - if (result == null) { - throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(name)); - } - return result; + public Map getValues(String name, Class keyClass, Class valueClass) { + return getValues(name, requireConverter(keyClass), requireConverter(valueClass)); + } + + public Map getValues(String name, Converter keyConverter, Converter valueConverter) { + return getValues(name, keyConverter, valueConverter, HashMap::new); } /** @@ -198,111 +205,179 @@ public Map getValues(String name, Class kClass, Class vClass) * @param The type of the values. * @return the resolved property value as an instance of the requested Map or {@code null} if it could not be found. * @throws IllegalArgumentException if a key or a value cannot be converted to the specified types + * @throws NoSuchElementException if no direct sub properties could be found. */ - public Map getValuesAsMap(String name, Converter keyConverter, Converter valueConverter) { - final String prefix = name.endsWith(".") ? name : name + "."; - final Map result = new HashMap<>(); + public Map getValues( + String name, + Converter keyConverter, + Converter valueConverter, + IntFunction> mapFactory) { + try { + return getValue(name, newMapConverter(keyConverter, valueConverter, mapFactory)); + } catch (NoSuchElementException e) { + Map mapKeys = getMapKeys(name); + if (mapKeys.isEmpty()) { + throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(name)); + } + + Map map = mapFactory.apply(mapKeys.size()); + for (Map.Entry entry : mapKeys.entrySet()) { + map.put(convertValue(ConfigValue.builder().withName(entry.getKey()).withValue(entry.getKey()).build(), + keyConverter), getValue(entry.getValue(), valueConverter)); + } + return map; + } + } + + public > Map getValues( + String name, + Class keyClass, + Class valueClass, + IntFunction collectionFactory) { + return getValues(name, requireConverter(keyClass), requireConverter(valueClass), HashMap::new, collectionFactory); + } + + public > Map getValues( + String name, + Converter keyConverter, + Converter valueConverter, + IntFunction> mapFactory, + IntFunction collectionFactory) { + try { + return getValue(name, + newMapConverter(keyConverter, newCollectionConverter(valueConverter, collectionFactory), mapFactory)); + } catch (NoSuchElementException e) { + Map mapCollectionKeys = getMapIndexedKeys(name); + if (mapCollectionKeys.isEmpty()) { + throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(name)); + } + + Map map = mapFactory.apply(mapCollectionKeys.size()); + for (Map.Entry entry : mapCollectionKeys.entrySet()) { + map.put(convertValue(ConfigValue.builder().withName(entry.getKey()).withValue(entry.getKey()).build(), + keyConverter), getValues(entry.getValue(), valueConverter, collectionFactory)); + } + return map; + } + } + + public Map getMapKeys(final String name) { + Map mapKeys = new HashMap<>(); for (String propertyName : getPropertyNames()) { - if (propertyName.startsWith(prefix)) { - final String key = propertyName.substring(prefix.length()); - if (key.indexOf('.') >= 0) { - // Ignore sub namespaces - continue; - } - result.put(convertValue(propertyName + "#key", key, keyConverter), - convertValue(propertyName + "#value", getRawValue(propertyName), valueConverter)); + if (propertyName.length() > name.length() + 1 + && (name.isEmpty() || propertyName.charAt(name.length()) == '.') + && propertyName.startsWith(name)) { + String key = unquoted(propertyName, name.isEmpty() ? 0 : name.length() + 1); + mapKeys.put(key, propertyName); } } - return result.isEmpty() ? null : result; + return mapKeys; + } + + public Map getMapIndexedKeys(final String name) { + Map mapKeys = new HashMap<>(); + for (String propertyName : getPropertyNames()) { + if (propertyName.length() > name.length() + 1 + && (name.isEmpty() || propertyName.charAt(name.length()) == '.') + && propertyName.startsWith(name)) { + String unindexedName = unindexed(propertyName); + String key = unquoted(unindexedName, name.isEmpty() ? 0 : name.length() + 1); + mapKeys.put(key, unindexedName); + } + } + return mapKeys; + } + + @Override + public T getValue(String name, Class aClass) { + return getValue(name, requireConverter(aClass)); } /** - * + * * This method handles calls from both {@link Config#getValue} and {@link Config#getOptionalValue}.
*/ @SuppressWarnings("unchecked") public T getValue(String name, Converter converter) { - final ConfigValue configValue = getConfigValue(name); - if (ConfigValueConverter.CONFIG_VALUE_CONVERTER.equals(converter)) { - return (T) configValue; + ConfigValue configValue = getConfigValue(name); + if (Converters.CONFIG_VALUE_CONVERTER.equals(converter)) { + return (T) configValue.noProblems(); } if (converter instanceof Converters.OptionalConverter) { - if (ConfigValueConverter.CONFIG_VALUE_CONVERTER.equals( + if (Converters.CONFIG_VALUE_CONVERTER.equals( ((Converters.OptionalConverter) converter).getDelegate())) { - return (T) Optional.of(configValue); + return (T) Optional.of(configValue.noProblems()); } } - final String value = configValue.getValue(); // Can return the empty String (which is not considered as null) - - return convertValue(name, value, converter); + return convertValue(configValue, converter); } /** - * * This method handles converting values for both CDI injections and programatical calls.
*
- * + * * Calls for converting non-optional values ({@link Config#getValue} and "Injecting Native Values") * should throw an {@link Exception} for each of the following:
- * + * * 1. {@link IllegalArgumentException} - if the property cannot be converted by the {@link Converter} to the specified type *
* 2. {@link NoSuchElementException} - if the property is not defined
* 3. {@link NoSuchElementException} - if the property is defined as an empty string
* 4. {@link NoSuchElementException} - if the {@link Converter} returns {@code null}
*
- * + * * Calls for converting optional values ({@link Config#getOptionalValue} and "Injecting Optional Values") * should only throw an {@link Exception} for #1 ({@link IllegalArgumentException} when the property cannot be converted to * the specified type). */ - public T convertValue(String name, String value, Converter converter) { + public T convertValue(ConfigValue configValue, Converter converter) { + if (configValue.hasProblems()) { + // TODO - Maybe it will depend on the problem, but we only get the expression NoSuchElement here for now + if (Converters.isOptionalConverter(converter)) { + configValue = configValue.noProblems(); + } else { + ConfigValidationException.Problem problem = configValue.getProblems().get(0); + Optional exception = problem.getException(); + if (exception.isPresent()) { + throw exception.get(); + } + } + } final T converted; - if (value != null) { + if (configValue.getValue() != null) { try { - converted = converter.convert(value); + converted = converter.convert(configValue.getValue()); } catch (IllegalArgumentException e) { - throw ConfigMessages.msg.converterException(e, name, value, e.getLocalizedMessage()); // 1 + throw ConfigMessages.msg.converterException(e, configValue.getNameProfiled(), configValue.getValue(), + e.getLocalizedMessage()); // 1 } } else { try { // See if the Converter is designed to handle a missing (null) value i.e. Optional Converters converted = converter.convert(""); } catch (IllegalArgumentException ignored) { - throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(name)); // 2 + throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(configValue.getNameProfiled())); // 2 } } if (converted == null) { - if (value == null) { - throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(name)); // 2 - } else if (value.length() == 0) { - throw ConfigMessages.msg.propertyEmptyString(name, converter.getClass().getTypeName()); // 3 + if (configValue.getValue() == null) { + throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(configValue.getNameProfiled())); // 2 + } else if (configValue.getValue().isEmpty()) { + throw ConfigMessages.msg.propertyEmptyString(configValue.getNameProfiled(), converter.getClass().getTypeName()); // 3 } else { - throw ConfigMessages.msg.converterReturnedNull(name, value, converter.getClass().getTypeName()); // 4 + throw ConfigMessages.msg.converterReturnedNull(configValue.getNameProfiled(), configValue.getValue(), + converter.getClass().getTypeName()); // 4 } } return converted; } - /** - * Determine whether the raw value of a configuration property is exactly equal to the expected given - * value. - * - * @param name the property name (must not be {@code null}) - * @param expected the expected value (may be {@code null}) - * @return {@code true} if the values are equal, {@code false} otherwise - */ - public boolean rawValueEquals(String name, String expected) { - return Objects.equals(expected, getRawValue(name)); - } - - @Experimental("Extension to the original ConfigSource to allow retrieval of additional metadata on config lookup") public ConfigValue getConfigValue(String name) { final ConfigValue configValue = configSources.getInterceptorChain().proceed(name); return configValue != null ? configValue : ConfigValue.builder().withName(name).build(); @@ -316,7 +391,7 @@ public ConfigValue getConfigValue(String name) { */ public String getRawValue(String name) { final ConfigValue configValue = getConfigValue(name); - return configValue != null && configValue.getValue() != null ? configValue.getValue() : null; + return configValue != null ? configValue.getValue() : null; } @Override @@ -324,24 +399,8 @@ public Optional getOptionalValue(String name, Class aClass) { return getValue(name, getOptionalConverter(aClass)); } - /** - * Return the content of the direct sub properties as the requested type of Map. - * - * @param name The configuration property name - * @param kClass the type into which the keys should be converted - * @param vClass the type into which the values should be converted - * @param the key type - * @param the value type - * @return the resolved property value as an instance of the requested Map (not {@code null}) - * @throws IllegalArgumentException if a key or a value cannot be converted to the specified types - */ - @Experimental("Extension to retrieve non mandatory sub properties as a Map") - public Optional> getOptionalValues(String name, Class kClass, Class vClass) { - return Optional.ofNullable(getValuesAsMap(name, requireConverter(kClass), requireConverter(vClass))); - } - public Optional getOptionalValue(String name, Converter converter) { - return getValue(name, Converters.newOptionalConverter(converter)); + return getValue(name, newOptionalConverter(converter)); } public Optional> getOptionalValues(final String propertyName, final Class propertyType) { @@ -355,8 +414,7 @@ public > Optional getOptionalValues(String name, C public > Optional getOptionalValues(String name, Converter converter, IntFunction collectionFactory) { - final Optional optionalValue = getOptionalValue(name, - Converters.newCollectionConverter(converter, collectionFactory)); + Optional optionalValue = getOptionalValue(name, newCollectionConverter(converter, collectionFactory)); if (optionalValue.isPresent()) { return optionalValue; } else { @@ -384,39 +442,146 @@ public > Optional getIndexedOptionalValues(String return Optional.empty(); } - public ConfigMappings getConfigMappings() { + /** + * Return the content of the direct sub properties as the requested type of Map. + * + * @param name The configuration property name + * @param keyClass the type into which the keys should be converted + * @param valueClass the type into which the values should be converted + * @param the key type + * @param the value type + * @return the resolved property value as an instance of the requested Map (not {@code null}) + * @throws IllegalArgumentException if a key or a value cannot be converted to the specified types + */ + public Optional> getOptionalValues(String name, Class keyClass, Class valueClass) { + return getOptionalValues(name, requireConverter(keyClass), requireConverter(valueClass)); + } + + public Optional> getOptionalValues(String name, Converter keyConverter, Converter valueConverter) { + return getOptionalValues(name, keyConverter, valueConverter, HashMap::new); + } + + public Optional> getOptionalValues(String name, Converter keyConverter, Converter valueConverter, + IntFunction> mapFactory) { + Map mapKeys = getMapKeys(name); + if (!mapKeys.isEmpty()) { + return Optional.of(getValues(name, keyConverter, valueConverter, mapFactory)); + } + + return getOptionalValue(name, newMapConverter(keyConverter, valueConverter, mapFactory)); + } + + public > Optional> getOptionalValues( + String name, + Class keyClass, + Class valueClass, + IntFunction collectionFactory) { + return getOptionalValues(name, requireConverter(keyClass), requireConverter(valueClass), HashMap::new, + collectionFactory); + } + + public > Optional> getOptionalValues( + String name, + Converter keyConverter, + Converter valueConverter, + IntFunction> mapFactory, + IntFunction collectionFactory) { + Optional> optionalValue = getOptionalValue(name, + newMapConverter(keyConverter, newCollectionConverter(valueConverter, collectionFactory), mapFactory)); + if (optionalValue.isPresent()) { + return optionalValue; + } + + Map mapKeys = getMapIndexedKeys(name); + if (mapKeys.isEmpty()) { + return Optional.empty(); + } + return Optional.of(getValues(name, keyConverter, valueConverter, mapFactory, collectionFactory)); + } + + Map, Map> getMappings() { return mappings; } - @Experimental("ConfigMapping API to group configuration properties") public T getConfigMapping(Class type) { - return mappings.getConfigMapping(type); + String prefix; + if (type.isInterface()) { + ConfigMapping configMapping = type.getAnnotation(ConfigMapping.class); + prefix = configMapping != null ? configMapping.prefix() : ""; + } else { + ConfigProperties configProperties = type.getAnnotation(ConfigProperties.class); + prefix = configProperties != null ? configProperties.prefix() : ""; + } + return getConfigMapping(type, prefix); } - @Experimental("ConfigMapping API to group configuration properties") public T getConfigMapping(Class type, String prefix) { - return mappings.getConfigMapping(type, prefix); + if (prefix == null) { + return getConfigMapping(type); + } + + Map mappingsForType = mappings.get(getConfigMappingClass(type)); + if (mappingsForType == null) { + throw ConfigMessages.msg.mappingNotFound(type.getName()); + } + + ConfigMappingObject configMappingObject = mappingsForType.get(prefix); + if (configMappingObject == null) { + throw ConfigMessages.msg.mappingPrefixNotFound(type.getName(), prefix); + } + + Object value = configMappingObject; + if (configMappingObject instanceof ConfigMappingClassMapper) { + value = ((ConfigMappingClassMapper) configMappingObject).map(); + } + + configValidator.validateMapping(type, prefix, value); + + return type.cast(value); } + /** + * {@inheritDoc} + * + * This implementation caches the list of property names collected when {@link SmallRyeConfig} is built via + * {@link SmallRyeConfigBuilder#build()}. + * + * @return the cached names of all configured keys of the underlying configuration + * @see SmallRyeConfig#getLatestPropertyNames() + */ @Override public Iterable getPropertyNames() { return configSources.getPropertyNames().get(); } /** - * Checks if a property is present in the {@link Config} instance. + * Provides a way to retrieve an updated list of all property names. The updated list replaces the cached list + * returned by {@link SmallRyeConfig#getPropertyNames()}. * + * @return the names of all configured keys of the underlying configuration + */ + @Experimental("Retrieve an updated list of all configuration property names") + public Iterable getLatestPropertyNames() { + return configSources.getPropertyNames().latest(); + } + + /** + * Checks if a property is present in the {@link Config} instance. + *
* Because {@link ConfigSource#getPropertyNames()} may not include all available properties, it is not possible to - * reliable determine if the property is present in the properties list. The property needs to be retrieved to make + * reliably determine if the property is present in the properties list. The property needs to be retrieved to make * sure it exists. The lookup is done without expression expansion, because the expansion value may not be - * available and it not relevant for the final check. + * available, and it is not relevant for the final check. * * @param name the property name. * @return true if the property is present or false otherwise. */ @Experimental("Check if a property is present") public boolean isPropertyPresent(String name) { - return Expressions.withoutExpansion(() -> getConfigValue(name).getValue() != null); + return Expressions.withoutExpansion(() -> { + ConfigValue configValue = SmallRyeConfig.this.getConfigValue(name); + return configValue.getValue() != null && !configValue.getValue().isEmpty(); + }); } @Override @@ -434,7 +599,6 @@ public Iterable getConfigSources(final Class type) { return configSourcesByType; } - @Experimental("To retrieve a ConfigSource by name") public Optional getConfigSource(final String name) { for (ConfigSource configSource : getConfigSources()) { final String configSourceName = configSource.getName(); @@ -449,10 +613,21 @@ public T convert(String value, Class asType) { return value != null ? requireConverter(asType).convert(value) : null; } - @SuppressWarnings({ "unchecked", "rawtypes" }) private Converter> getOptionalConverter(Class asType) { - return optionalConverters.computeIfAbsent(asType, - clazz -> Converters.newOptionalConverter(requireConverter((Class) clazz))); + Converter> converter = recast(optionalConverters.get(asType)); + if (converter == null) { + converter = newOptionalConverter(requireConverter(asType)); + Converter> appearing = recast(optionalConverters.putIfAbsent(asType, recast(converter))); + if (appearing != null) { + converter = appearing; + } + } + return converter; + } + + @SuppressWarnings("unchecked") + private static T recast(Object obj) { + return (T) obj; } @Deprecated // binary-compatibility bridge method for Quarkus @@ -498,13 +673,16 @@ public T unwrap(final Class type) { throw ConfigMessages.msg.getTypeNotSupportedForUnwrapping(type); } - @Experimental("To retrieve active profiles") public List getProfiles() { return configSources.getProfiles(); } - void addPropertyNames(Set properties) { - configSources.getPropertyNames().add(properties); + public ConfigSource getDefaultValues() { + return configSources.defaultValues; + } + + ConfigSourceInterceptorContext interceptorChain() { + return configSources.interceptorChain; } private static class ConfigSources implements Serializable { @@ -512,6 +690,7 @@ private static class ConfigSources implements Serializable { private final List profiles; private final List sources; + private final ConfigSource defaultValues; private final ConfigSourceInterceptorContext interceptorChain; private final PropertyNames propertyNames; @@ -520,64 +699,87 @@ private static class ConfigSources implements Serializable { * that this constructor must be used when the Config object is being initialized, because interceptors also * require initialization. */ - ConfigSources(final SmallRyeConfigBuilder builder) { + ConfigSources(final SmallRyeConfigBuilder builder, final SmallRyeConfig config) { // Add all sources except for ConfigurableConfigSource types. These are initialized later List sources = buildSources(builder); + // Add the default values sources separately, so we can keep a reference to it and add mappings defaults + DefaultValuesConfigSource defaultValues = new DefaultValuesConfigSource(builder.getDefaultValues()); + sources.add(defaultValues); + // Add all interceptors - List interceptors = new ArrayList<>(); + List negativeInterceptors = new ArrayList<>(); + List positiveInterceptors = new ArrayList<>(); + SmallRyeConfigSources negativeSources = new SmallRyeConfigSources(mapSources(sources), true); + SmallRyeConfigSources positiveSources = new SmallRyeConfigSources(mapSources(sources), false); List interceptorWithPriorities = buildInterceptors(builder); // Create the initial chain with initial sources and all interceptors - SmallRyeConfigSourceInterceptorContext current = new SmallRyeConfigSourceInterceptorContext(EMPTY, null); - current = new SmallRyeConfigSourceInterceptorContext(new SmallRyeConfigSources(mapSources(sources)), current); + SmallRyeConfigSourceInterceptorContext current = new SmallRyeConfigSourceInterceptorContext(EMPTY, null, config); + current = new SmallRyeConfigSourceInterceptorContext(negativeSources, current, config); + for (InterceptorWithPriority interceptorWithPriority : interceptorWithPriorities) { + if (interceptorWithPriority.getPriority() < 0) { + ConfigSourceInterceptor interceptor = interceptorWithPriority.getInterceptor(current); + negativeInterceptors.add(interceptor); + current = new SmallRyeConfigSourceInterceptorContext(interceptor, current, config); + } + } + current = new SmallRyeConfigSourceInterceptorContext(positiveSources, current, config); for (InterceptorWithPriority interceptorWithPriority : interceptorWithPriorities) { - ConfigSourceInterceptor interceptor = interceptorWithPriority.getInterceptor(current); - interceptors.add(interceptor); - current = new SmallRyeConfigSourceInterceptorContext(interceptor, current); + if (interceptorWithPriority.getPriority() >= 0) { + ConfigSourceInterceptor interceptor = interceptorWithPriority.getInterceptor(current); + positiveInterceptors.add(interceptor); + current = new SmallRyeConfigSourceInterceptorContext(interceptor, current, config); + } } // Init all late sources - List profiles = getProfiles(interceptors); - List sourcesWithPriorities = mapLateSources(sources, interceptors, current, profiles, - builder); + List profiles = getProfiles(positiveInterceptors); + List sourcesWithPriorities = mapLateSources(sources, negativeInterceptors, + positiveInterceptors, current, profiles, builder, config); + List configSources = getSources(sourcesWithPriorities); // Rebuild the chain with the late sources and new instances of the interceptors // The new instance will ensure that we get rid of references to factories and other stuff and keep only // the resolved final source or interceptor to use. - current = new SmallRyeConfigSourceInterceptorContext(EMPTY, null); - current = new SmallRyeConfigSourceInterceptorContext(new SmallRyeConfigSources(sourcesWithPriorities), current); - for (ConfigSourceInterceptor interceptor : interceptors) { - current = new SmallRyeConfigSourceInterceptorContext(interceptor, current); + current = new SmallRyeConfigSourceInterceptorContext(EMPTY, null, config); + current = new SmallRyeConfigSourceInterceptorContext(new SmallRyeConfigSources(sourcesWithPriorities, true), + current, config); + for (ConfigSourceInterceptor interceptor : negativeInterceptors) { + current = new SmallRyeConfigSourceInterceptorContext(interceptor, current, config); + } + current = new SmallRyeConfigSourceInterceptorContext(new SmallRyeConfigSources(sourcesWithPriorities, false), + current, config); + for (ConfigSourceInterceptor interceptor : positiveInterceptors) { + current = new SmallRyeConfigSourceInterceptorContext(interceptor, current, config); } - - // PropertyNames and generate additional properties - List configSources = getSources(sourcesWithPriorities); - PropertyNamesConfigSourceInterceptor propertyNamesInterceptor = new PropertyNamesConfigSourceInterceptor(); - current = new SmallRyeConfigSourceInterceptorContext(propertyNamesInterceptor, current); - PropertyNames propertyNames = new PropertyNames(propertyNamesInterceptor); - propertyNames.add(generateDottedProperties(configSources, current)); this.profiles = profiles; this.sources = configSources; + this.defaultValues = defaultValues; this.interceptorChain = current; - this.propertyNames = propertyNames; + this.propertyNames = new PropertyNames(); } private static List buildSources(final SmallRyeConfigBuilder builder) { - final List sourcesToBuild = new ArrayList<>(builder.getSources()); + List sourcesToBuild = new ArrayList<>(builder.getSources()); + for (ConfigSourceProvider sourceProvider : builder.getSourceProviders()) { + for (ConfigSource configSource : sourceProvider.getConfigSources(builder.getClassLoader())) { + sourcesToBuild.add(configSource); + } + } + if (builder.isAddDiscoveredSources()) { sourcesToBuild.addAll(builder.discoverSources()); } if (builder.isAddDefaultSources()) { sourcesToBuild.addAll(builder.getDefaultSources()); } - sourcesToBuild.add(new DefaultValuesConfigSource(builder.getDefaultValues())); return sourcesToBuild; } private static List buildInterceptors(final SmallRyeConfigBuilder builder) { - final List interceptors = new ArrayList<>(builder.getInterceptors()); + List interceptors = new ArrayList<>(builder.getInterceptors()); if (builder.isAddDiscoveredInterceptors()) { interceptors.addAll(builder.discoverInterceptors()); } @@ -590,7 +792,7 @@ private static List buildInterceptors(final SmallRyeCon } private static List mapSources(final List sources) { - final List sourcesWithPriority = new ArrayList<>(); + List sourcesWithPriority = new ArrayList<>(); for (ConfigSource source : sources) { if (!(source instanceof ConfigurableConfigSource)) { sourcesWithPriority.add(new ConfigSourceWithPriority(source)); @@ -602,7 +804,7 @@ private static List mapSources(final List getProfiles(final List interceptors) { - for (final ConfigSourceInterceptor interceptor : interceptors) { + for (ConfigSourceInterceptor interceptor : interceptors) { if (interceptor instanceof ProfileConfigSourceInterceptor) { return Arrays.asList(((ProfileConfigSourceInterceptor) interceptor).getProfiles()); } @@ -612,17 +814,19 @@ private static List getProfiles(final List inte private static List mapLateSources( final List sources, - final List interceptors, + final List negativeInterceptors, + final List positiveInterceptors, final ConfigSourceInterceptorContext current, final List profiles, - final SmallRyeConfigBuilder builder) { + final SmallRyeConfigBuilder builder, + final SmallRyeConfig config) { ConfigSourceWithPriority.resetLoadPriority(); List currentSources = new ArrayList<>(); // Init all profile sources first List profileSources = new ArrayList<>(); - ConfigSourceContext mainContext = new SmallRyeConfigSourceContext(current, profiles); + ConfigSourceContext mainContext = new SmallRyeConfigSourceContext(current, profiles, sources); for (ConfigurableConfigSource profileSource : getConfigurableSources(sources)) { if (profileSource.getFactory() instanceof ProfileConfigSourceFactory) { profileSources.addAll(profileSource.getConfigSources(mainContext)); @@ -636,16 +840,23 @@ private static List mapLateSources( Collections.reverse(currentSources); // Rebuild the chain with the profiles sources, so profiles values are also available in factories - ConfigSourceInterceptorContext context = new SmallRyeConfigSourceInterceptorContext(EMPTY, null); - context = new SmallRyeConfigSourceInterceptorContext(new SmallRyeConfigSources(currentSources), context); - for (ConfigSourceInterceptor interceptor : interceptors) { - context = new SmallRyeConfigSourceInterceptorContext(interceptor, context); + ConfigSourceInterceptorContext context = new SmallRyeConfigSourceInterceptorContext(EMPTY, null, config); + context = new SmallRyeConfigSourceInterceptorContext(new SmallRyeConfigSources(currentSources, true), context, + config); + for (ConfigSourceInterceptor interceptor : negativeInterceptors) { + context = new SmallRyeConfigSourceInterceptorContext(interceptor, context, config); + } + context = new SmallRyeConfigSourceInterceptorContext(new SmallRyeConfigSources(currentSources, false), context, + config); + for (ConfigSourceInterceptor interceptor : positiveInterceptors) { + context = new SmallRyeConfigSourceInterceptorContext(interceptor, context, config); } // Init remaining sources, coming from SmallRyeConfig int countSourcesFromLocations = 0; List lateSources = new ArrayList<>(); - ConfigSourceContext profileContext = new SmallRyeConfigSourceContext(context, profiles); + ConfigSourceContext profileContext = new SmallRyeConfigSourceContext(context, profiles, + currentSources.stream().map(ConfigSourceWithPriority::getSource).collect(toList())); for (ConfigurableConfigSource lateSource : getConfigurableSources(sources)) { if (!(lateSource.getFactory() instanceof ProfileConfigSourceFactory)) { List configSources = lateSource.getConfigSources(profileContext); @@ -679,7 +890,11 @@ private static List mapLateSources( private static List getSources(final List sourceWithPriorities) { List configSources = new ArrayList<>(); for (ConfigSourceWithPriority configSourceWithPriority : sourceWithPriorities) { - configSources.add(configSourceWithPriority.getSource()); + ConfigSource source = configSourceWithPriority.getSource(); + configSources.add(source); + if (ConfigLogging.log.isDebugEnabled()) { + ConfigLogging.log.loadedConfigSource(source.getName(), source.getOrdinal()); + } } return Collections.unmodifiableList(configSources); } @@ -695,62 +910,6 @@ private static List getConfigurableSources(final List< return Collections.unmodifiableList(configurableConfigSources); } - /** - * Generate dotted properties from Env properties. - * - * These are required when a consumer relies on the list of properties to find additional - * configurations. The list of properties is not normalized due to environment variables, which follow specific - * naming rules. The MicroProfile Config specification defines a set of conversion rules to look up and find - * values from environment variables even when using their dotted version, but this does not apply to the - * properties list. - * - * Because an environment variable name may only be represented by a subset of characters, it is not possible - * to represent exactly a dotted version name from an environment variable name. Additional dotted properties - * mapped from environment variables are only added if a relationship cannot be found between all properties - * using the conversions look up rules of the MicroProfile Config specification. Example: - * - * If foo.bar is present and FOO_BAR is also present, no property is required. - * If foo-bar is present and FOO_BAR is also present, no property is required. - * If FOO_BAR is present a property foo.bar is required. - */ - private static Set generateDottedProperties(final List sources, - final SmallRyeConfigSourceInterceptorContext current) { - // Collect all known properties - Set properties = new HashSet<>(); - Iterator iterateNames = current.iterateNames(); - while (iterateNames.hasNext()) { - properties.add(iterateNames.next()); - } - - // Collect only properties from the EnvSources - Set envProperties = new HashSet<>(); - for (ConfigSource source : sources) { - if (source instanceof EnvConfigSource) { - envProperties.addAll(source.getPropertyNames()); - } - } - properties.removeAll(envProperties); - - // Collect properties that have the same semantic meaning - Set overrides = new HashSet<>(); - for (String property : properties) { - for (String envProperty : envProperties) { - if (envProperty.equalsIgnoreCase(replaceNonAlphanumericByUnderscores(property))) { - overrides.add(envProperty); - break; - } - } - } - - // Remove them - Remaining properties can only be found in the EnvSource - generate a dotted version - envProperties.removeAll(overrides); - Set dottedProperties = new HashSet<>(); - for (String envProperty : envProperties) { - dottedProperties.add(toLowerCaseAndDotted(envProperty)); - } - return dottedProperties; - } - List getProfiles() { return profiles; } @@ -770,23 +929,23 @@ PropertyNames getPropertyNames() { class PropertyNames implements Serializable { private static final long serialVersionUID = 4193517748286869745L; - private final PropertyNamesConfigSourceInterceptor interceptor; - - private PropertyNames(final PropertyNamesConfigSourceInterceptor propertyNamesInterceptor) { - this.interceptor = propertyNamesInterceptor; - } + private final Set names = new HashSet<>(); Iterable get() { - final HashSet names = new HashSet<>(); - final Iterator namesIterator = interceptorChain.iterateNames(); - while (namesIterator.hasNext()) { - names.add(namesIterator.next()); + if (names.isEmpty()) { + return latest(); } return names; } - void add(final Set properties) { - interceptor.addProperties(properties); + Iterable latest() { + names.clear(); + Iterator namesIterator = interceptorChain.iterateNames(); + while (namesIterator.hasNext()) { + names.add(namesIterator.next()); + } + names.remove(ConfigSource.CONFIG_ORDINAL); + return Collections.unmodifiableSet(names); } } } @@ -807,6 +966,10 @@ ConfigSource getSource() { return source; } + int priority() { + return priority; + } + @Override public int compareTo(final ConfigSourceWithPriority other) { int res = Integer.compare(this.priority, other.priority); @@ -820,6 +983,42 @@ static void resetLoadPriority() { } } + private static class SmallRyeConfigSourceContext implements ConfigSourceContext { + private final ConfigSourceInterceptorContext context; + private final List profiles; + private final List sources; + + public SmallRyeConfigSourceContext( + final ConfigSourceInterceptorContext context, + final List profiles, + final List sources) { + this.context = context; + this.profiles = profiles; + this.sources = sources; + } + + @Override + public ConfigValue getValue(final String name) { + ConfigValue value = context.proceed(name); + return value != null ? value : ConfigValue.builder().withName(name).build(); + } + + @Override + public List getProfiles() { + return profiles; + } + + @Override + public List getConfigSources() { + return sources; + } + + @Override + public Iterator iterateNames() { + return context.iterateNames(); + } + } + private Object writeReplace() throws ObjectStreamException { return RegisteredConfig.instance; } diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java index fab73fe0b..438198d72 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java @@ -17,15 +17,24 @@ package io.smallrye.config; import static io.smallrye.config.ConfigSourceInterceptorFactory.DEFAULT_PRIORITY; +import static io.smallrye.config.Converters.STRING_CONVERTER; +import static io.smallrye.config.Converters.newCollectionConverter; +import static io.smallrye.config.Converters.newTrimmingConverter; +import static io.smallrye.config.ProfileConfigSourceInterceptor.convertProfile; import static io.smallrye.config.PropertiesConfigSourceProvider.classPathSources; +import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_LOG_VALUES; +import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_PROFILE; +import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_PROFILE_PARENT; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.OptionalInt; @@ -34,7 +43,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.annotation.Priority; +import jakarta.annotation.Priority; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigBuilder; @@ -42,12 +51,15 @@ import org.eclipse.microprofile.config.spi.ConfigSourceProvider; import org.eclipse.microprofile.config.spi.Converter; +import io.smallrye.config._private.ConfigMessages; + /** * @author Jeff Mesnil (c) 2017 Red Hat inc. */ public class SmallRyeConfigBuilder implements ConfigBuilder { public static final String META_INF_MICROPROFILE_CONFIG_PROPERTIES = "META-INF/microprofile-config.properties"; + private final List customizers = new ArrayList<>(); // sources are not sorted by their ordinals private final List sources = new ArrayList<>(); private final List sourceProviders = new ArrayList<>(); @@ -55,18 +67,27 @@ public class SmallRyeConfigBuilder implements ConfigBuilder { private final List profiles = new ArrayList<>(); private final Set secretKeys = new HashSet<>(); private final List interceptors = new ArrayList<>(); - private final KeyMap defaultValues = new KeyMap<>(); - private final ConfigMappingProvider.Builder mappingsBuilder = ConfigMappingProvider.builder(); + private final List secretKeysHandlers = new ArrayList<>(); private ConfigValidator validator = ConfigValidator.EMPTY; + private final Map defaultValues = new HashMap<>(); + private final ConfigMappingProvider.Builder mappingsBuilder = ConfigMappingProvider.builder(); private ClassLoader classLoader = SecuritySupport.getContextClassLoader(); + private boolean addDiscoveredCustomizers = false; private boolean addDefaultSources = false; private boolean addDefaultInterceptors = false; private boolean addDiscoveredSources = false; private boolean addDiscoveredConverters = false; private boolean addDiscoveredInterceptors = false; + private boolean addDiscoveredSecretKeysHandlers = false; private boolean addDiscoveredValidator = false; public SmallRyeConfigBuilder() { + withMappingDefaults(true); + } + + public SmallRyeConfigBuilder addDiscoveredCustomizers() { + addDiscoveredCustomizers = true; + return this; } @Override @@ -86,6 +107,11 @@ public SmallRyeConfigBuilder addDiscoveredInterceptors() { return this; } + public SmallRyeConfigBuilder addDiscoveredSecretKeysHandlers() { + addDiscoveredSecretKeysHandlers = true; + return this; + } + public SmallRyeConfigBuilder addDiscoveredValidator() { addDiscoveredValidator = true; return this; @@ -150,6 +176,10 @@ ConfigValidator discoverValidator() { return ConfigValidator.EMPTY; } + ConfigMappingProvider.Builder getMappingsBuilder() { + return mappingsBuilder; + } + @Override public SmallRyeConfigBuilder addDefaultSources() { addDefaultSources = true; @@ -177,20 +207,66 @@ List getDefaultInterceptors() { interceptors.add(new InterceptorWithPriority(new ConfigSourceInterceptorFactory() { @Override public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context) { - return profiles.isEmpty() ? new ProfileConfigSourceInterceptor(context) - : new ProfileConfigSourceInterceptor(profiles); + if (profiles.isEmpty()) { + profiles.addAll(getProfile(context)); + } + return new ProfileConfigSourceInterceptor(profiles); } @Override public OptionalInt getPriority() { return OptionalInt.of(Priorities.LIBRARY + 200); } + + private List getProfile(final ConfigSourceInterceptorContext context) { + Set profiles = new LinkedHashSet<>(); + profiles.addAll(getProfiles(context, SMALLRYE_CONFIG_PROFILE_PARENT)); + profiles.addAll(getProfiles(context, SMALLRYE_CONFIG_PROFILE)); + return new ArrayList<>(profiles); + } + + private List getProfiles(final ConfigSourceInterceptorContext context, final String propertyName) { + List profiles = new ArrayList<>(); + ConfigValue profileValue = context.proceed(propertyName); + if (profileValue != null) { + final List convertProfiles = convertProfile(profileValue.getValue()); + for (String profile : convertProfiles) { + profiles.addAll(getProfiles(context, "%" + profile + "." + SMALLRYE_CONFIG_PROFILE_PARENT)); + profiles.add(profile); + } + } + return profiles; + } })); interceptors.add(new InterceptorWithPriority(new ConfigSourceInterceptorFactory() { @Override public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context) { - final Map relocations = new HashMap<>(); + Map relocations = new HashMap<>(); relocations.put(SmallRyeConfig.SMALLRYE_CONFIG_PROFILE, Config.PROFILE); + + List multipleProfileProperties = new ArrayList<>(); + Iterator names = context.iterateNames(); + while (names.hasNext()) { + String name = names.next(); + if (!name.isEmpty() && name.charAt(0) == '%') { + NameIterator ni = new NameIterator(name); + String profileSegment = ni.getNextSegment(); + List profiles = convertProfile(profileSegment.substring(1)); + if (profiles.size() > 1) { + multipleProfileProperties + .add(new MultipleProfileProperty(name, name.substring(profileSegment.length()), profiles)); + } + } + } + + // Ordered properties by least number of profiles. Priority to the ones with most specific profiles. + for (MultipleProfileProperty multipleProfileProperty : multipleProfileProperties) { + for (String profile : multipleProfileProperty.getProfiles()) { + relocations.putIfAbsent("%" + profile + multipleProfileProperty.getRelocateName(), + multipleProfileProperty.getName()); + } + } + return new RelocateConfigSourceInterceptor(relocations); } @@ -198,11 +274,45 @@ public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorConte public OptionalInt getPriority() { return OptionalInt.of(Priorities.LIBRARY + 200 - 1); } + + class MultipleProfileProperty implements Comparable { + private final String name; + private final String relocateName; + private final List profiles; + + public MultipleProfileProperty(final String name, final String relocateName, final List profiles) { + this.name = name; + this.relocateName = relocateName; + this.profiles = profiles; + } + + public String getName() { + return name; + } + + public String getRelocateName() { + return relocateName; + } + + public List getProfiles() { + return profiles; + } + + @Override + public int compareTo(final MultipleProfileProperty o) { + return Integer.compare(this.getProfiles().size(), o.getProfiles().size()); + } + } })); interceptors.add(new InterceptorWithPriority(new ConfigSourceInterceptorFactory() { @Override public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context) { - return new ExpressionConfigSourceInterceptor(context); + boolean expressions = true; + ConfigValue expressionsValue = context.proceed(Config.PROPERTY_EXPRESSIONS_ENABLED); + if (expressionsValue != null) { + expressions = Boolean.parseBoolean(expressionsValue.getValue()); + } + return new ExpressionConfigSourceInterceptor(expressions); } @Override @@ -210,7 +320,109 @@ public OptionalInt getPriority() { return OptionalInt.of(Priorities.LIBRARY + 300); } })); - interceptors.add(new InterceptorWithPriority(new SecretKeysConfigSourceInterceptor(secretKeys))); + + interceptors.add(new InterceptorWithPriority(new ConfigSourceInterceptorFactory() { + @Override + public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context) { + return new SecretKeysConfigSourceInterceptor(SmallRyeConfigBuilder.this.secretKeys); + } + + @Override + public OptionalInt getPriority() { + return OptionalInt.of(Priorities.LIBRARY + 100); + } + })); + interceptors.add(new InterceptorWithPriority(new ConfigSourceInterceptorFactory() { + @Override + public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context) { + List secretKeysHandlers = new ArrayList<>(); + for (SecretKeysHandlerWithName secretKeysHandler : SmallRyeConfigBuilder.this.secretKeysHandlers) { + secretKeysHandlers.add(secretKeysHandler.getSecretKeysHandler(new ConfigSourceContext() { + @Override + public ConfigValue getValue(final String name) { + return context.proceed(name); + } + + @Override + public Iterator iterateNames() { + return context.iterateNames(); + } + })); + } + + if (isAddDiscoveredSecretKeysHandlers()) { + secretKeysHandlers.addAll(discoverSecretKeysHandlers(context)); + } + return new SecretKeysHandlerConfigSourceInterceptor(true, secretKeysHandlers); + } + + @Override + public OptionalInt getPriority() { + return OptionalInt.of(Priorities.LIBRARY + 310); + } + + private List getEnabledHandlers(final ConfigSourceInterceptorContext context) { + ConfigValue enabledHandlers = context.proceed("smallrye.config.secret-handlers"); + if (enabledHandlers == null || enabledHandlers.getValue().equals("all")) { + return List.of(); + } + + List handlers = newCollectionConverter(newTrimmingConverter(STRING_CONVERTER), ArrayList::new) + .convert(enabledHandlers.getValue()); + return handlers != null ? handlers : List.of(); + } + + private List discoverSecretKeysHandlers(final ConfigSourceInterceptorContext context) { + List enabledHandlers = getEnabledHandlers(context); + + List discoveredHandlers = new ArrayList<>(); + ServiceLoader secretKeysHandlers = ServiceLoader.load(SecretKeysHandler.class, classLoader); + for (SecretKeysHandler secretKeysHandler : secretKeysHandlers) { + if (enabledHandlers.isEmpty() || enabledHandlers.contains(secretKeysHandler.getName())) { + discoveredHandlers.add(secretKeysHandler); + } + } + + ServiceLoader secretKeysHandlerFactories = ServiceLoader + .load(SecretKeysHandlerFactory.class, classLoader); + for (SecretKeysHandlerFactory secretKeysHandlerFactory : secretKeysHandlerFactories) { + if (enabledHandlers.isEmpty() || enabledHandlers.contains(secretKeysHandlerFactory.getName())) { + discoveredHandlers.add( + secretKeysHandlerFactory + .getSecretKeysHandler(new ConfigSourceContext() { + @Override + public ConfigValue getValue(final String name) { + return context.proceed(name); + } + + @Override + public Iterator iterateNames() { + return context.iterateNames(); + } + })); + } + } + + return discoveredHandlers; + } + })); + + interceptors.add(new InterceptorWithPriority(new ConfigSourceInterceptorFactory() { + @Override + public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context) { + boolean enabled = false; + ConfigValue enabledValue = context.proceed(SMALLRYE_CONFIG_LOG_VALUES); + if (enabledValue != null) { + enabled = Boolean.parseBoolean(enabledValue.getValue()); + } + return new LoggingConfigSourceInterceptor(enabled); + } + + @Override + public OptionalInt getPriority() { + return OptionalInt.of(Priorities.LIBRARY + 250); + } + })); return interceptors; } @@ -221,6 +433,11 @@ public SmallRyeConfigBuilder forClassLoader(ClassLoader classLoader) { return this; } + public SmallRyeConfigBuilder withCustomizers(SmallRyeConfigBuilderCustomizer... customizers) { + Collections.addAll(this.customizers, customizers); + return this; + } + @Override public SmallRyeConfigBuilder withSources(ConfigSource... configSources) { Collections.addAll(sources, configSources); @@ -258,9 +475,23 @@ public SmallRyeConfigBuilder withInterceptorFactories(ConfigSourceInterceptorFac return this; } + public SmallRyeConfigBuilder withSecretKeysHandlers(SecretKeysHandler... secretKeysHandlers) { + for (SecretKeysHandler secretKeysHandler : secretKeysHandlers) { + this.secretKeysHandlers.add(new SecretKeysHandlerWithName(secretKeysHandler)); + } + return this; + } + + public SmallRyeConfigBuilder withSecretKeyHandlerFactories(SecretKeysHandlerFactory... secretKeyHandlerFactories) { + for (SecretKeysHandlerFactory secretKeyHandlerFactory : secretKeyHandlerFactories) { + this.secretKeysHandlers.add(new SecretKeysHandlerWithName(secretKeyHandlerFactory)); + } + return this; + } + public SmallRyeConfigBuilder withProfile(String profile) { addDefaultInterceptors(); - this.profiles.addAll(ProfileConfigSourceInterceptor.convertProfile(profile)); + this.profiles.addAll(convertProfile(profile)); return this; } @@ -276,14 +507,12 @@ public SmallRyeConfigBuilder withSecretKeys(String... keys) { } public SmallRyeConfigBuilder withDefaultValue(String name, String value) { - this.defaultValues.findOrAdd(name).putRootValue(value); + this.defaultValues.put(name, value); return this; } public SmallRyeConfigBuilder withDefaultValues(Map defaultValues) { - for (Map.Entry entry : defaultValues.entrySet()) { - this.defaultValues.findOrAdd(entry.getKey()).putRootValue(entry.getValue()); - } + this.defaultValues.putAll(defaultValues); return this; } @@ -297,7 +526,7 @@ public SmallRyeConfigBuilder withMapping(Class klass, String prefix) { } public SmallRyeConfigBuilder withMappingIgnore(String path) { - mappingsBuilder.addIgnored(path); + mappingsBuilder.ignoredPath(path); return this; } @@ -307,6 +536,21 @@ public SmallRyeConfigBuilder withValidateUnknown(boolean validateUnknown) { return this; } + public SmallRyeConfigBuilder withMappingDefaults(boolean mappingDefaults) { + mappingsBuilder.registerDefaults(mappingDefaults ? this : null); + return this; + } + + public SmallRyeConfigBuilder withMappingNames(final Map>> names) { + mappingsBuilder.names(names); + return this; + } + + public SmallRyeConfigBuilder withMappingKeys(final Set keys) { + mappingsBuilder.keys(keys); + return this; + } + public SmallRyeConfigBuilder withValidator(ConfigValidator validator) { this.validator = validator; return this; @@ -368,6 +612,10 @@ public List getInterceptors() { return interceptors; } + public List getProfiles() { + return profiles; + } + public ConfigValidator getValidator() { if (isAddDiscoveredValidator()) { this.validator = discoverValidator(); @@ -375,10 +623,18 @@ public ConfigValidator getValidator() { return validator; } - public KeyMap getDefaultValues() { + public Map getDefaultValues() { return defaultValues; } + public ClassLoader getClassLoader() { + return classLoader; + } + + public boolean isAddDiscoveredCustomizers() { + return addDiscoveredCustomizers; + } + public boolean isAddDefaultSources() { return addDefaultSources; } @@ -399,6 +655,10 @@ public boolean isAddDiscoveredInterceptors() { return addDiscoveredInterceptors; } + public boolean isAddDiscoveredSecretKeysHandlers() { + return addDiscoveredSecretKeysHandlers; + } + public boolean isAddDiscoveredValidator() { return addDiscoveredValidator; } @@ -428,6 +688,11 @@ public SmallRyeConfigBuilder setAddDiscoveredInterceptors(final boolean addDisco return this; } + public SmallRyeConfigBuilder setAddDiscoveredSecretKeysHandlers(final boolean addDiscoveredSecretKeysHandlers) { + this.addDiscoveredSecretKeysHandlers = addDiscoveredSecretKeysHandlers; + return this; + } + public SmallRyeConfigBuilder setAddDiscoveredValidator(final boolean addDiscoveredValidator) { this.addDiscoveredValidator = addDiscoveredValidator; return this; @@ -435,23 +700,18 @@ public SmallRyeConfigBuilder setAddDiscoveredValidator(final boolean addDiscover @Override public SmallRyeConfig build() { - for (ConfigSourceProvider sourceProvider : sourceProviders) { - for (ConfigSource configSource : sourceProvider.getConfigSources(classLoader)) { - sources.add(configSource); + if (addDiscoveredCustomizers) { + for (SmallRyeConfigBuilderCustomizer customizer : ServiceLoader.load(SmallRyeConfigBuilderCustomizer.class, + classLoader)) { + customizers.add(customizer); } } - ConfigMappingProvider mappingProvider = mappingsBuilder.build(); - defaultValues.putAll(mappingProvider.getDefaultValues()); + customizers.stream() + .sorted(Comparator.comparingInt(SmallRyeConfigBuilderCustomizer::priority)) + .forEach(customizer -> customizer.configBuilder(SmallRyeConfigBuilder.this)); - try { - ConfigMappings configMappings = new ConfigMappings(getValidator()); - SmallRyeConfig config = new SmallRyeConfig(this, configMappings); - mappingProvider.mapConfiguration(config); - return config; - } catch (ConfigValidationException e) { - throw new IllegalStateException(e); - } + return new SmallRyeConfig(this); } static class ConverterWithPriority { @@ -496,6 +756,18 @@ public OptionalInt getPriority() { this.priority = factory.getPriority().orElse(DEFAULT_PRIORITY); } + InterceptorWithPriority(ConfigSourceInterceptor interceptor, int priority) { + this(new ConfigSourceInterceptorFactory() { + public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context) { + return interceptor; + } + + public OptionalInt getPriority() { + return OptionalInt.of(priority); + } + }); + } + ConfigSourceInterceptor getInterceptor(ConfigSourceInterceptorContext context) { return factory.getInterceptor(context); } @@ -523,4 +795,34 @@ private static int getPriority(final Class kl } } } + + static class SecretKeysHandlerWithName { + private final SecretKeysHandlerFactory factory; + + SecretKeysHandlerWithName(SecretKeysHandler secretKeysHandler) { + this(new SecretKeysHandlerFactory() { + @Override + public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context) { + return secretKeysHandler; + } + + @Override + public String getName() { + return secretKeysHandler.getName(); + } + }); + } + + SecretKeysHandlerWithName(SecretKeysHandlerFactory factory) { + this.factory = factory; + } + + io.smallrye.config.SecretKeysHandler getSecretKeysHandler(ConfigSourceContext context) { + return factory.getSecretKeysHandler(context); + } + + String getName() { + return factory.getName(); + } + } } diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilderCustomizer.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilderCustomizer.java new file mode 100644 index 000000000..18e6b99b3 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilderCustomizer.java @@ -0,0 +1,28 @@ +package io.smallrye.config; + +/** + * This {@code SmallRyeConfigBuilderCustomizer} allows to customize a {@link SmallRyeConfigBuilder}, used to create + * a {@link SmallRyeConfig} instance. + *

+ * Instances of this interface will be discovered via the {@link java.util.ServiceLoader} mechanism and can be + * registered by providing a {@code META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer} which + * contains the fully qualified class name of the custom {@link SmallRyeConfigBuilderCustomizer} implementation. + */ +public interface SmallRyeConfigBuilderCustomizer { + /** + * Customize the current {@link SmallRyeConfigBuilder}. + * + * @param builder the current {@link SmallRyeConfigBuilder}. + */ + void configBuilder(SmallRyeConfigBuilder builder); + + /** + * Returns the customizer priority. Customizers are sorted by ascending priority and executed in that order, meaning + * that higher numeric priorities will be executing last, possible overriding values set by previous customizers. + * + * @return the priority value. + */ + default int priority() { + return 0; + } +} diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigFactory.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigFactory.java index f45ac2ad3..b7bd6be68 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigFactory.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigFactory.java @@ -51,6 +51,7 @@ static final class Default extends SmallRyeConfigFactory { public SmallRyeConfig getConfigFor(SmallRyeConfigProviderResolver configProviderResolver, ClassLoader classLoader) { return configProviderResolver.getBuilder().forClassLoader(classLoader) + .addDiscoveredCustomizers() .addDefaultSources() .addDefaultInterceptors() .addDiscoveredSources() diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigProviderResolver.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigProviderResolver.java index 01a3e3ab0..a45e1eca5 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigProviderResolver.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigProviderResolver.java @@ -27,6 +27,8 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import io.smallrye.config._private.ConfigMessages; + /** * @author Jeff Mesnil (c) 2017 Red Hat inc. * @author David M. Lloyd @@ -130,6 +132,14 @@ public void releaseConfig(Config config) { } } + public void releaseConfig(ClassLoader classLoader) { + final ClassLoader realClassLoader = getRealClassLoader(classLoader); + final Map configsForClassLoader = this.configsForClassLoader; + synchronized (configsForClassLoader) { + configsForClassLoader.remove(realClassLoader); + } + } + static ClassLoader getRealClassLoader(ClassLoader classLoader) { if (classLoader == null) { classLoader = getContextClassLoader(); diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigSourceContext.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigSourceContext.java deleted file mode 100644 index 8c781ff42..000000000 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigSourceContext.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.smallrye.config; - -import java.util.Iterator; -import java.util.List; - -class SmallRyeConfigSourceContext implements ConfigSourceContext { - private final ConfigSourceInterceptorContext context; - private final List profiles; - - public SmallRyeConfigSourceContext(final ConfigSourceInterceptorContext context, final List profiles) { - this.context = context; - this.profiles = profiles; - } - - @Override - public ConfigValue getValue(final String name) { - ConfigValue value = context.proceed(name); - return value != null ? value : ConfigValue.builder().withName(name).build(); - } - - @Override - public List getProfiles() { - return profiles; - } - - @Override - public Iterator iterateNames() { - return context.iterateNames(); - } -} diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigSourceInterceptorContext.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigSourceInterceptorContext.java index db6eb1e40..6643fa804 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigSourceInterceptorContext.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigSourceInterceptorContext.java @@ -7,12 +7,17 @@ class SmallRyeConfigSourceInterceptorContext implements ConfigSourceInterceptorC private final ConfigSourceInterceptor interceptor; private final ConfigSourceInterceptorContext next; + private final SmallRyeConfig config; + + private static final ThreadLocal rcHolder = ThreadLocal.withInitial(RecursionCount::new); SmallRyeConfigSourceInterceptorContext( final ConfigSourceInterceptor interceptor, - final ConfigSourceInterceptorContext next) { + final ConfigSourceInterceptorContext next, + final SmallRyeConfig config) { this.interceptor = interceptor; this.next = next; + this.config = config; } @Override @@ -20,13 +25,38 @@ public ConfigValue proceed(final String name) { return interceptor.getValue(next, name); } + @Override + public ConfigValue restart(final String name) { + RecursionCount rc = rcHolder.get(); + rc.increment(); + try { + return config.interceptorChain().proceed(name); + } finally { + if (rc.decrement()) { + // avoid leaking if the thread is cached + rcHolder.remove(); + } + } + } + @Override public Iterator iterateNames() { return interceptor.iterateNames(next); } - @Override - public Iterator iterateValues() { - return interceptor.iterateValues(next); + static final class RecursionCount { + int count; + + void increment() { + int old = count; + if (old == 20) { + throw new IllegalStateException("Too many recursive interceptor actions"); + } + count = old + 1; + } + + boolean decrement() { + return --count == 0; + } } } diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigSources.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigSources.java index 308a8f639..17deef2a2 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigSources.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigSources.java @@ -1,5 +1,6 @@ package io.smallrye.config; +import java.io.Serializable; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; @@ -7,17 +8,23 @@ import java.util.Map; import java.util.Set; +import org.eclipse.microprofile.config.spi.ConfigSource; + import io.smallrye.config.SmallRyeConfig.ConfigSourceWithPriority; class SmallRyeConfigSources implements ConfigSourceInterceptor { private static final long serialVersionUID = 7560201715403486552L; private final List configSources; + private final boolean negative; - SmallRyeConfigSources(final List configSourcesWithPriorities) { + SmallRyeConfigSources(final List configSourcesWithPriorities, boolean negative) { + this.negative = negative; List configSources = new ArrayList<>(); for (ConfigSourceWithPriority configSource : configSourcesWithPriorities) { - configSources.add(ConfigValueConfigSourceWrapper.wrap(configSource.getSource())); + if ((configSource.priority() < 0) == negative) { + configSources.add(ConfigValueConfigSourceWrapper.wrap(configSource.getSource())); + } } this.configSources = configSources; } @@ -31,7 +38,7 @@ public ConfigValue getValue(final ConfigSourceInterceptorContext context, final return configValue.from().withConfigSourcePosition(i).build(); } } - return null; + return context.proceed(name); } @Override @@ -43,18 +50,78 @@ public Iterator iterateNames(final ConfigSourceInterceptorContext contex names.addAll(propertyNames); } } + Iterator iter = context.iterateNames(); + iter.forEachRemaining(names::add); return names.iterator(); } - @Override - public Iterator iterateValues(final ConfigSourceInterceptorContext context) { - final Set values = new HashSet<>(); - for (final ConfigValueConfigSource configSource : configSources) { - final Map configValueProperties = configSource.getConfigValueProperties(); - if (configValueProperties != null) { - values.addAll(configValueProperties.values()); + boolean negative() { + return negative; + } + + static final class ConfigValueConfigSourceWrapper implements ConfigValueConfigSource, Serializable { + private static final long serialVersionUID = -1109094614437147326L; + + private final ConfigSource configSource; + + private ConfigValueConfigSourceWrapper(final ConfigSource configSource) { + this.configSource = configSource; + } + + @Override + public ConfigValue getConfigValue(final String propertyName) { + String value = configSource.getValue(propertyName); + if (value != null) { + return ConfigValue.builder() + .withName(propertyName) + .withValue(value) + .withRawValue(value) + .withConfigSourceName(getName()) + .withConfigSourceOrdinal(getOrdinal()) + .build(); + } + + return null; + } + + @Override + public Map getConfigValueProperties() { + return new ConfigValueMapStringView(configSource.getProperties(), + configSource.getName(), + configSource.getOrdinal()); + } + + @Override + public Map getProperties() { + return configSource.getProperties(); + } + + @Override + public String getValue(final String propertyName) { + return configSource.getValue(propertyName); + } + + @Override + public Set getPropertyNames() { + return configSource.getPropertyNames(); + } + + @Override + public String getName() { + return configSource.getName(); + } + + @Override + public int getOrdinal() { + return configSource.getOrdinal(); + } + + static ConfigValueConfigSource wrap(final ConfigSource configSource) { + if (configSource instanceof ConfigValueConfigSource) { + return (ConfigValueConfigSource) configSource; + } else { + return new ConfigValueConfigSourceWrapper(configSource); } } - return values.iterator(); } } diff --git a/implementation/src/main/java/io/smallrye/config/WithConverter.java b/implementation/src/main/java/io/smallrye/config/WithConverter.java index 0c63ab64b..e12d4d39a 100644 --- a/implementation/src/main/java/io/smallrye/config/WithConverter.java +++ b/implementation/src/main/java/io/smallrye/config/WithConverter.java @@ -8,15 +8,12 @@ import org.eclipse.microprofile.config.spi.Converter; -import io.smallrye.common.annotation.Experimental; - /** * Specify the converter to use to convert the annotated type. */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE_USE }) -@Experimental("ConfigMapping API to group configuration properties") public @interface WithConverter { /** * The converter class to use. diff --git a/implementation/src/main/java/io/smallrye/config/WithDefault.java b/implementation/src/main/java/io/smallrye/config/WithDefault.java index c94dd34fa..c49e1fbf5 100644 --- a/implementation/src/main/java/io/smallrye/config/WithDefault.java +++ b/implementation/src/main/java/io/smallrye/config/WithDefault.java @@ -6,15 +6,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import io.smallrye.common.annotation.Experimental; - /** * Specify the default value of a property. */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.FIELD }) -@Experimental("ConfigMapping API to group configuration properties") public @interface WithDefault { /** * The default value of the property. diff --git a/implementation/src/main/java/io/smallrye/config/WithDefaults.java b/implementation/src/main/java/io/smallrye/config/WithDefaults.java new file mode 100644 index 000000000..107c58bfd --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/WithDefaults.java @@ -0,0 +1,17 @@ +package io.smallrye.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation intented to be used only in a Map mapping to allow the Map to return + * the default value for the value element on any key lookup. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD }) +public @interface WithDefaults { +} diff --git a/implementation/src/main/java/io/smallrye/config/WithName.java b/implementation/src/main/java/io/smallrye/config/WithName.java index 63560e801..f197f0159 100644 --- a/implementation/src/main/java/io/smallrye/config/WithName.java +++ b/implementation/src/main/java/io/smallrye/config/WithName.java @@ -6,15 +6,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import io.smallrye.common.annotation.Experimental; - /** * The name of the configuration property or group. */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.FIELD }) -@Experimental("ConfigMapping API to group configuration properties") public @interface WithName { /** * The name of the property or group. Must not be empty. diff --git a/implementation/src/main/java/io/smallrye/config/WithParentName.java b/implementation/src/main/java/io/smallrye/config/WithParentName.java index ef8770da1..a0769c30d 100644 --- a/implementation/src/main/java/io/smallrye/config/WithParentName.java +++ b/implementation/src/main/java/io/smallrye/config/WithParentName.java @@ -6,14 +6,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import io.smallrye.common.annotation.Experimental; - /** * Use the parent's configuration name. */ @Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) -@Experimental("ConfigMapping API to group configuration properties") public @interface WithParentName { } diff --git a/implementation/src/main/java/io/smallrye/config/WithUnnamedKey.java b/implementation/src/main/java/io/smallrye/config/WithUnnamedKey.java new file mode 100644 index 000000000..4ab3df275 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/WithUnnamedKey.java @@ -0,0 +1,23 @@ +package io.smallrye.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Omits a single map key from the configuration name when populating {@link java.util.Map} types. Configuration values + * for the {@link java.util.Map} may be retrieved by the key defined in {@link WithUnnamedKey#value()}. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE_USE }) +public @interface WithUnnamedKey { + /** + * The key name to use to populate configuration values from a configuration path without a key. + * + * @return the Map key + */ + String value() default ""; +} diff --git a/implementation/src/main/java/io/smallrye/config/ConfigLogging.java b/implementation/src/main/java/io/smallrye/config/_private/ConfigLogging.java similarity index 86% rename from implementation/src/main/java/io/smallrye/config/ConfigLogging.java rename to implementation/src/main/java/io/smallrye/config/_private/ConfigLogging.java index 8951d776f..46c516f7f 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigLogging.java +++ b/implementation/src/main/java/io/smallrye/config/_private/ConfigLogging.java @@ -1,4 +1,4 @@ -package io.smallrye.config; +package io.smallrye.config._private; import org.jboss.logging.BasicLogger; import org.jboss.logging.Logger; @@ -9,7 +9,7 @@ @MessageLogger(projectCode = "SRCFG", length = 5) public interface ConfigLogging extends BasicLogger { - ConfigLogging log = Logger.getMessageLogger(ConfigLogging.class, ConfigLogging.class.getPackage().getName()); + ConfigLogging log = Logger.getMessageLogger(ConfigLogging.class, "io.smallrye.config"); @LogMessage(level = Logger.Level.WARN) @Message(id = 1000, value = "Unable to get context classloader instance") @@ -34,4 +34,8 @@ public interface ConfigLogging extends BasicLogger { @LogMessage(level = Logger.Level.WARN) @Message(id = 1005, value = "Could not find sources with %s in %s") void configLocationsNotFound(String name, String value); + + @LogMessage(level = Logger.Level.DEBUG) + @Message(id = 1006, value = "Loaded ConfigSource %s with ordinal %d") + void loadedConfigSource(String name, int ordinal); } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMessages.java b/implementation/src/main/java/io/smallrye/config/_private/ConfigMessages.java similarity index 87% rename from implementation/src/main/java/io/smallrye/config/ConfigMessages.java rename to implementation/src/main/java/io/smallrye/config/_private/ConfigMessages.java index f3ea5c01f..51a24aa72 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMessages.java +++ b/implementation/src/main/java/io/smallrye/config/_private/ConfigMessages.java @@ -1,4 +1,4 @@ -package io.smallrye.config; +package io.smallrye.config._private; import java.io.InvalidObjectException; import java.lang.reflect.Type; @@ -121,8 +121,8 @@ public interface ConfigMessages { @Message(id = 34, value = "URI Syntax invalid %s") IllegalArgumentException uriSyntaxInvalid(@Cause Throwable cause, String uri); - @Message(id = 35, value = "Failed to load resource") - IllegalStateException failedToLoadResource(@Cause Throwable cause); + @Message(id = 35, value = "Failed to load resource %s") + IllegalArgumentException failedToLoadResource(@Cause Throwable cause, String location); @Message(id = 36, value = "Type %s not supported for unwrapping.") IllegalArgumentException getTypeNotSupportedForUnwrapping(Class type); @@ -154,4 +154,19 @@ IllegalArgumentException converterException(@Cause Throwable converterException, @Message(id = 45, value = "The %s class is not a ConfigMapping") IllegalArgumentException classIsNotAMapping(Class type); + + @Message(id = 46, value = "Could not find a secret key handler for %s") + NoSuchElementException secretKeyHandlerNotFound(String handler); + + @Message(id = 47, value = "The ConfigMapping path %s is ambiguous. It is mapped by %s and %s") + IllegalStateException ambiguousMapping(String path, String amb1, String amb2); + + @Message(id = 48, value = "The config property %s explicitly defined the key %s, but the key is marked as unnamed") + IllegalArgumentException explicitNameInUnnamed(String name, String key); + + @Message(id = 49, value = "Cannot convert %s to enum %s, allowed values: %s") + IllegalArgumentException cannotConvertEnum(String value, Class enumType, String values); + + @Message(id = 50, value = "%s in %s does not map to any root") + IllegalStateException propertyDoesNotMapToAnyRoot(String name, String location); } diff --git a/implementation/src/test/java/io/smallrye/config/BuilderReuseTest.java b/implementation/src/test/java/io/smallrye/config/BuilderReuseTest.java index 9836e11b8..5e0857c09 100644 --- a/implementation/src/test/java/io/smallrye/config/BuilderReuseTest.java +++ b/implementation/src/test/java/io/smallrye/config/BuilderReuseTest.java @@ -26,7 +26,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import javax.annotation.Priority; +import jakarta.annotation.Priority; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigSource; diff --git a/implementation/src/test/java/io/smallrye/config/ConfigConfigSourceTest.java b/implementation/src/test/java/io/smallrye/config/ConfigConfigSourceTest.java index 2038f4277..209682834 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigConfigSourceTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigConfigSourceTest.java @@ -200,7 +200,7 @@ public ConfigValue getValue(final ConfigSourceInterceptorContext context, final @Override public OptionalInt getPriority() { - return OptionalInt.of(Integer.MIN_VALUE); + return OptionalInt.of(0); } }) .withSources(new MapBackedConfigSource("test", new HashMap() { diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingClassTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingClassTest.java index a32867edf..8b2423502 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingClassTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingClassTest.java @@ -15,36 +15,36 @@ class ConfigMappingClassTest { @Test void toClass() { - final SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerClass.class) + SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerClass.class) .withDefaultValue("host", "localhost") .withDefaultValue("port", "8080") .build(); - final ServerClass server = config.getConfigMapping(ServerClass.class); + ServerClass server = config.getConfigMapping(ServerClass.class); assertEquals("localhost", server.getHost()); assertEquals(8080, server.getPort()); } @Test void privateFields() { - final SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerPrivate.class) + SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerPrivate.class) .withDefaultValue("host", "localhost") .withDefaultValue("port", "8080") .build(); - final ServerPrivate server = config.getConfigMapping(ServerPrivate.class); + ServerPrivate server = config.getConfigMapping(ServerPrivate.class); assertEquals("localhost", server.getHost()); assertEquals(8080, server.getPort()); } @Test void optionals() { - final SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerOptional.class) + SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerOptional.class) .withDefaultValue("host", "localhost") .withDefaultValue("port", "8080") .build(); - final ServerOptional server = config.getConfigMapping(ServerOptional.class); + ServerOptional server = config.getConfigMapping(ServerOptional.class); assertTrue(server.getHost().isPresent()); assertEquals("localhost", server.getHost().get()); assertTrue(server.getPort().isPresent()); @@ -53,38 +53,38 @@ void optionals() { @Test void names() { - final SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerNames.class) + SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerNames.class) .withDefaultValue("h", "localhost") .withDefaultValue("p", "8080") .build(); - final ServerNames server = config.getConfigMapping(ServerNames.class); + ServerNames server = config.getConfigMapping(ServerNames.class); assertEquals("localhost", server.getHost()); assertEquals(8080, server.getPort()); } @Test void defaults() { - final SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerDefaults.class).build(); - final ServerDefaults server = config.getConfigMapping(ServerDefaults.class); + SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerDefaults.class).build(); + ServerDefaults server = config.getConfigMapping(ServerDefaults.class); assertEquals("localhost", server.getHost()); assertEquals(8080, server.getPort()); } @Test void converters() { - final SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerConverters.class) + SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerConverters.class) .withConverters(new Converter[] { new ServerPortConverter() }).build(); - final ServerConverters server = config.getConfigMapping(ServerConverters.class); + ServerConverters server = config.getConfigMapping(ServerConverters.class); assertEquals("localhost", server.getHost()); assertEquals(8080, server.getPort().getPort()); } @Test void initialized() { - final SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerInitialized.class) + SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerInitialized.class) .withDefaultValue("host", "localhost") .build(); - final ServerInitialized server = config.getConfigMapping(ServerInitialized.class); + ServerInitialized server = config.getConfigMapping(ServerInitialized.class); assertEquals("localhost", server.getHost()); assertEquals(8080, server.getPort()); } @@ -96,10 +96,10 @@ void initializedDefault() { @Test void mpConfig20() { - final SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerMPConfig20.class) + SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerMPConfig20.class) .withDefaultValue("name", "localhost") .build(); - final ServerMPConfig20 server = config.getConfigMapping(ServerMPConfig20.class); + ServerMPConfig20 server = config.getConfigMapping(ServerMPConfig20.class); assertEquals("localhost", server.getHost()); assertEquals(8080, server.getPort()); } @@ -115,12 +115,12 @@ void empty() { @Test void camelCase() { - final SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerCamelCase.class) + SmallRyeConfig config = new SmallRyeConfigBuilder().withMapping(ServerCamelCase.class) .withDefaultValue("theHost", "localhost") .withDefaultValue("thePort", "8080") .build(); - final ServerCamelCase server = config.getConfigMapping(ServerCamelCase.class); + ServerCamelCase server = config.getConfigMapping(ServerCamelCase.class); assertEquals("localhost", server.getTheHost()); assertEquals(8080, server.getThePort()); } @@ -213,7 +213,7 @@ public ServerPort getPort() { static class ServerPort { private int port; - public ServerPort(final String port) { + public ServerPort(String port) { this.port = Integer.parseInt(port); } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java index d4f1758a2..8e0c13dde 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingCollectionsTest.java @@ -5,6 +5,7 @@ import static java.util.stream.Collectors.toSet; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; @@ -215,7 +216,7 @@ interface App { } @Test - void mappingCollectionsOptionals() throws Exception { + void mappingCollectionsOptionals() { SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(ServerCollectionsOptionals.class, "server") .withSources(config( @@ -535,7 +536,7 @@ void mapWithColllections() { .withSources(config("map.roles.user[0]", "p1")) .withSources(config("map.roles.admin[0]", "p2", "map.roles.admin[1]", "p3")) .withSources(config("map.alias.user[0]", "p1")) - .withSources(config("map.alias.admin[0]", "p2", "map.roles.admin[1]", "p2")) + .withSources(config("map.alias.admin[0]", "p2", "map.alias.admin[1]", "p2")) .build(); MapWithCollections mapping = config.getConfigMapping(MapWithCollections.class); @@ -570,11 +571,11 @@ void mapWithListsGroup() { .withSources(config( "map.roles.user[0].name", "p1", "map.roles.user[0].aliases[0]", "user-role-p1", - "map.roles.user[0].permissions.read[0]", "read")) + "map.roles.user[0].permissions.read", "read")) .withSources(config( "map.roles.admin[0].name", "p2", "map.roles.admin[0].aliases", "admin-role-p2,administrator-role-p2", - "map.roles.admin[0].permissions.read[0]", "read", + "map.roles.admin[0].permissions.read", "read", "map.roles.admin[1].name", "p3", "map.roles.admin[1].aliases", "admin-role-p3,administrator-role-p3", "map.roles.admin[1].permissions.write[0]", "create", @@ -648,39 +649,290 @@ void mapWithListsAndParentName() { assertEquals("root", mapping.aliases().get("admin").alias().get(1)); } - @ConfigMapping(prefix = "maps") - public interface NestedMaps { - Map> values(); + @ConfigMapping(prefix = "map") + interface MapIndexedAndPlain { + @WithParentName + Map> map(); + } - Map>> roles(); + @Test + void mapIndexedAndPlain() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(MapIndexedAndPlain.class, "map") + .withSources(config( + "map.one[0]", "one", "map.one[1]", "1", + "map.two", "two,2")) + .build(); - Map>> aliases(); + MapIndexedAndPlain mapping = config.getConfigMapping(MapIndexedAndPlain.class); - interface Aliases { - String name(); + assertEquals("one", mapping.map().get("one").get(0)); + assertEquals("1", mapping.map().get("one").get(1)); + assertEquals("two", mapping.map().get("two").get(0)); + } + + @Test + void simpleMap() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(SimpleMap.class, "map") + .withSources(config("map.one", "value")) + .withSources(config("map.key-converter.key", "value")) + .withSources(config("map.value-converter.one", "something")) + .withSources(config("map.defaults.one", "value")) + .withSources(config("map.defaults-value-converter.one", "something")) + .build(); + + SimpleMap mapping = config.getConfigMapping(SimpleMap.class); + + assertEquals("value", mapping.map().get("one")); + assertNull(mapping.map().get("any")); + assertEquals("value", mapping.keyConverter().get("one")); + assertNull(mapping.keyConverter().get("any")); + assertEquals("value", mapping.valueConverter().get("one")); + assertNull(mapping.valueConverter().get("any")); + assertEquals("value", mapping.defaults().get("one")); + assertEquals("any", mapping.defaults().get("any")); + assertEquals("value", mapping.defaultsValueConverter().get("one")); + assertEquals("value", mapping.defaultsValueConverter().get("any")); + assertTrue(mapping.defaultsOnly().isEmpty()); + assertEquals("any", mapping.defaultsOnly().get("any")); + } + + @ConfigMapping(prefix = "map") + interface SimpleMap { + @WithParentName + Map map(); + + Map<@WithConverter(KeyConverter.class) String, String> keyConverter(); + + Map valueConverter(); + + @WithDefault("any") + Map defaults(); + + @WithDefault("any") + Map defaultsValueConverter(); + + @WithDefault("any") + Map defaultsOnly(); + + class KeyConverter implements Converter { + @Override + public String convert(final String value) throws IllegalArgumentException, NullPointerException { + return "one"; + } + } + + class ValueConverter implements Converter { + @Override + public String convert(final String value) throws IllegalArgumentException, NullPointerException { + return "value"; + } } } @Test void nestedMaps() { SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "nested.simple.one", "one")) + .withSources(config( + "nested.map.one.two", "one-two", + "nested.map.1.2", "1-2", + "nested.map.one.two.three", "one-two-three", + "nested.map.\"1.one\".\"2.two\"", "1.one-2.two")) + .withSources(config( + "nested.key-converter.\"1.one\".\"2.two\"", "1.one-2.two")) + .withSources(config( + "nested.key-implicit.1.1", "1-1", + "nested.key-implicit.1.\"2\"", "1-2")) + .withSources(config( + "nested.value-converter.one.one", "one-one", + "nested.value-converter.one.\"two\"", "one-two")) + .withSources(config( + "nested.defaults.one.one", "one-one", + "nested.defaults.one.\"two\"", "one-two")) + .withSources(config( + "nested.triple.one.two.one", "one-two-one", + "nested.triple.one.two.two", "one-two-two", + "nested.triple.one.two.three", "one-two-three", + "nested.triple.1.2.3", "1-2-3", + "nested.triple.\"1.one\".\"2.two\".\"3.three\"", "1.one-2.two-3.three")) .withMapping(NestedMaps.class) + .build(); + + NestedMaps mapping = config.getConfigMapping(NestedMaps.class); + + assertEquals("one", mapping.simple().get("one")); + + assertEquals("one-two", mapping.map().get("one").get("two")); + assertEquals("1-2", mapping.map().get("1").get("2")); + assertEquals("one-two-three", mapping.map().get("one").get("two.three")); + assertEquals("1.one-2.two", mapping.map().get("1.one").get("2.two")); + + assertEquals("1.one-2.two", mapping.keyConverter().get("one").get("one")); + + assertEquals("1-1", mapping.keyImplicit().get(1).get(1)); + assertEquals("1-2", mapping.keyImplicit().get(1).get(2)); + + assertEquals("value", mapping.valueConverter().get("one").get("one")); + assertEquals("value", mapping.valueConverter().get("one").get("two")); + + assertEquals("one-one", mapping.defaults().get("one").get("one")); + assertEquals("one-two", mapping.defaults().get("one").get("two")); + assertEquals("any", mapping.defaults().get("one").get("three")); + // TODO - Add defaults for middle maps? + //assertEquals("any", mapping.defaults().get("any").get("any")); + + assertEquals("one-two-one", mapping.triple().get("one").get("two").get("one")); + assertEquals("one-two-two", mapping.triple().get("one").get("two").get("two")); + assertEquals("one-two-three", mapping.triple().get("one").get("two").get("three")); + assertEquals("1-2-3", mapping.triple().get("1").get("2").get("3")); + assertEquals("1.one-2.two-3.three", mapping.triple().get("1.one").get("2.two").get("3.three")); + + // TODO - Assert keys that do not complete the Map nesting levels + } + + @ConfigMapping(prefix = "nested") + public interface NestedMaps { + Map simple(); + + Map> map(); + + Map<@WithConverter(KeyConverter.class) String, Map<@WithConverter(KeyConverter.class) String, String>> keyConverter(); + + Map> keyImplicit(); + + Map> valueConverter(); + + @WithDefault("any") + Map> defaults(); + + Map>> triple(); + + class KeyConverter implements Converter { + @Override + public String convert(final String value) throws IllegalArgumentException, NullPointerException { + return "one"; + } + } + + class ValueConverter implements Converter { + @Override + public String convert(final String value) throws IllegalArgumentException, NullPointerException { + return "value"; + } + } + } + + @Test + void simpleMapList() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(SimpleMapList.class, "map") + .withSources(config("map.one[0]", "value")) + .withSources(config("map.key-converter.key[0]", "value")) + .withSources(config("map.value-converter.one[0]", "something")) + .withSources(config("map.defaults.one[0]", "value")) + .withSources(config("map.defaults-value-converter.one[0]", "something")) + .build(); + + SimpleMapList mapping = config.getConfigMapping(SimpleMapList.class); + + assertEquals("value", mapping.map().get("one").get(0)); + assertNull(mapping.map().get("any")); + assertEquals("value", mapping.keyConverter().get("one").get(0)); + assertNull(mapping.keyConverter().get("any")); + assertEquals("value", mapping.valueConverter().get("one").get(0)); + assertNull(mapping.valueConverter().get("any")); + assertEquals("value", mapping.defaults().get("one").get(0)); + assertEquals("any", mapping.defaults().get("any").get(0)); + assertEquals("something", mapping.defaults().get("any").get(1)); + assertEquals("value", mapping.defaultsValueConverter().get("one").get(0)); + assertEquals("value", mapping.defaultsValueConverter().get("any").get(0)); + assertTrue(mapping.defaultsOnly().isEmpty()); + assertEquals("any", mapping.defaultsOnly().get("any").get(0)); + assertEquals("something", mapping.defaultsOnly().get("any").get(1)); + } + + @ConfigMapping(prefix = "map") + interface SimpleMapList { + @WithParentName + Map> map(); + + Map<@WithConverter(KeyConverter.class) String, List> keyConverter(); + + Map> valueConverter(); + + @WithDefault("any,something") + Map> defaults(); + + @WithDefault("any") + Map> defaultsValueConverter(); + + @WithDefault("any,something") + Map> defaultsOnly(); + + class KeyConverter implements Converter { + @Override + public String convert(final String value) throws IllegalArgumentException, NullPointerException { + return "one"; + } + } + + class ValueConverter implements Converter { + @Override + public String convert(final String value) throws IllegalArgumentException, NullPointerException { + return "value"; + } + } + } + + @Test + void nestedMapsLists() { + SmallRyeConfig config = new SmallRyeConfigBuilder() .withSources(config( - "maps.values.key1.nested-key1", "value")) + "nested.simple.one[1]", "one[1]", + "nested.simple.one[0]", "one[0]")) .withSources(config( - "maps.roles.user.crud[0]", "read", - "maps.roles.user.crud[1]", "write")) + "nested.map.one[1].two", "one[1]-two", + "nested.map.one[0].two", "one[0]-two")) .withSources(config( - "maps.aliases.user.system[0].name", "username", - "maps.aliases.user.system[1].name", "login")) + "nested.key-converter.1[0].two", "one[0]-two")) + .withSources(config( + "nested.mixed.one[0].one[0].one", "one[0].one[0].one", + "nested.mixed.one[0].one[0].two", "one[0].one[0].two")) + .withMapping(NestedMapsLists.class) .build(); - NestedMaps configMapping = config.getConfigMapping(NestedMaps.class); + NestedMapsLists mapping = config.getConfigMapping(NestedMapsLists.class); + + assertEquals("one[0]", mapping.simple().get("one").get(0)); + assertEquals("one[1]", mapping.simple().get("one").get(1)); + + assertEquals("one[0]-two", mapping.map().get("one").get(0).get("two")); + assertEquals("one[1]-two", mapping.map().get("one").get(1).get("two")); + + assertEquals("one[0]-two", mapping.keyConverter().get("one").get(0).get("two")); + + assertEquals("one[0].one[0].one", mapping.mixed().get("one").get(0).get("one").get(0).get("one")); + assertEquals("one[0].one[0].two", mapping.mixed().get("one").get(0).get("one").get(0).get("two")); + } + + @ConfigMapping(prefix = "nested") + interface NestedMapsLists { + Map> simple(); + + Map>> map(); + + Map<@WithConverter(KeyConverter.class) String, List>> keyConverter(); - assertEquals("value", configMapping.values().get("key1").get("nested-key1")); - assertEquals("read", configMapping.roles().get("user").get("crud").get(0)); - assertEquals("write", configMapping.roles().get("user").get("crud").get(1)); - assertEquals("username", configMapping.aliases().get("user").get("system").get(0).name()); - assertEquals("login", configMapping.aliases().get("user").get("system").get(1).name()); + Map>>>> mixed(); + + class KeyConverter implements Converter { + @Override + public String convert(final String value) throws IllegalArgumentException, NullPointerException { + return "one"; + } + } } } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingDefaultsTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingDefaultsTest.java new file mode 100644 index 000000000..bd5396503 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingDefaultsTest.java @@ -0,0 +1,728 @@ +package io.smallrye.config; + +import static io.smallrye.config.ConfigMappingDefaultsTest.S3BuildTimeConfig.AsyncHttpClientBuildTimeConfig.AsyncClientType.NETTY; +import static io.smallrye.config.ConfigMappingDefaultsTest.S3BuildTimeConfig.SyncHttpClientBuildTimeConfig.SyncClientType.URL; +import static io.smallrye.config.ConfigMappings.getDefaults; +import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; +import static io.smallrye.config.KeyValuesConfigSource.config; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import io.smallrye.config.ConfigMappingDefaultsTest.DataSourcesJdbcBuildTimeConfig.DataSourceJdbcOuterNamedBuildTimeConfig; + +public class ConfigMappingDefaultsTest { + @Test + void defaults() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "defaults.list-nested[0].value", "value", + "defaults.parent.list-nested[0].value", "value")) + .withMapping(Defaults.class) + .build(); + + Defaults mapping = config.getConfigMapping(Defaults.class); + + assertEquals("value", mapping.value()); + assertEquals(10, mapping.primitive()); + assertTrue(mapping.optional().isPresent()); + assertEquals("value", mapping.optional().get()); + assertTrue(mapping.optionalPrimitive().isPresent()); + assertEquals(10, mapping.optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.list()); + assertEquals("value", mapping.map().get("default")); + + assertEquals("value", mapping.nested().value()); + assertEquals(10, mapping.nested().primitive()); + assertTrue(mapping.nested().optional().isPresent()); + assertEquals("value", mapping.nested().optional().get()); + assertTrue(mapping.nested().optionalPrimitive().isPresent()); + assertEquals(10, mapping.nested().optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.nested().list()); + assertEquals("value", mapping.nested().map().get("default")); + + assertTrue(mapping.optionalNested().isPresent()); + assertEquals("value", mapping.optionalNested().get().value()); + assertEquals(10, mapping.optionalNested().get().primitive()); + assertTrue(mapping.optionalNested().get().optional().isPresent()); + assertEquals("value", mapping.optionalNested().get().optional().get()); + assertTrue(mapping.optionalNested().get().optionalPrimitive().isPresent()); + assertEquals(10, mapping.optionalNested().get().optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.optionalNested().get().list()); + assertEquals("value", mapping.optionalNested().get().map().get("default")); + + assertEquals("value", mapping.listNested().get(0).value()); + assertEquals(10, mapping.listNested().get(0).primitive()); + assertTrue(mapping.listNested().get(0).optional().isPresent()); + assertEquals("value", mapping.listNested().get(0).optional().get()); + assertTrue(mapping.listNested().get(0).optionalPrimitive().isPresent()); + assertEquals(10, mapping.listNested().get(0).optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.listNested().get(0).list()); + assertEquals("value", mapping.listNested().get(0).map().get("default")); + + assertEquals("value", mapping.mapNested().get("default").value()); + assertEquals(10, mapping.mapNested().get("default").primitive()); + assertTrue(mapping.mapNested().get("default").optional().isPresent()); + assertEquals("value", mapping.mapNested().get("default").optional().get()); + assertTrue(mapping.mapNested().get("default").optionalPrimitive().isPresent()); + assertEquals(10, mapping.mapNested().get("default").optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.mapNested().get("default").list()); + assertEquals("value", mapping.mapNested().get("default").map().get("default")); + + assertEquals("value", mapping.parent().child().nested().value()); + assertEquals(10, mapping.parent().child().nested().primitive()); + assertTrue(mapping.parent().child().nested().optional().isPresent()); + assertEquals("value", mapping.parent().child().nested().optional().get()); + assertTrue(mapping.parent().child().nested().optionalPrimitive().isPresent()); + assertEquals(10, mapping.parent().child().nested().optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.parent().child().nested().list()); + assertEquals("value", mapping.parent().child().nested().map().get("default")); + + assertTrue(mapping.parent().child().optionalNested().isPresent()); + assertEquals("value", mapping.parent().child().optionalNested().get().value()); + assertEquals(10, mapping.parent().child().optionalNested().get().primitive()); + assertTrue(mapping.parent().child().optionalNested().get().optional().isPresent()); + assertEquals("value", mapping.parent().child().optionalNested().get().optional().get()); + assertTrue(mapping.parent().child().optionalNested().get().optionalPrimitive().isPresent()); + assertEquals(10, mapping.parent().child().optionalNested().get().optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.parent().child().optionalNested().get().list()); + assertEquals("value", mapping.parent().child().optionalNested().get().map().get("default")); + + assertEquals("value", mapping.parent().child().listNested().get(0).value()); + assertEquals(10, mapping.parent().child().listNested().get(0).primitive()); + assertTrue(mapping.parent().child().listNested().get(0).optional().isPresent()); + assertEquals("value", mapping.parent().child().listNested().get(0).optional().get()); + assertTrue(mapping.parent().child().listNested().get(0).optionalPrimitive().isPresent()); + assertEquals(10, mapping.parent().child().listNested().get(0).optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.parent().child().listNested().get(0).list()); + assertEquals("value", mapping.parent().child().listNested().get(0).map().get("default")); + + assertEquals("value", mapping.parent().child().mapNested().get("default").value()); + assertEquals(10, mapping.parent().child().mapNested().get("default").primitive()); + assertTrue(mapping.parent().child().mapNested().get("default").optional().isPresent()); + assertEquals("value", mapping.parent().child().mapNested().get("default").optional().get()); + assertTrue(mapping.parent().child().mapNested().get("default").optionalPrimitive().isPresent()); + assertEquals(10, mapping.parent().child().mapNested().get("default").optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.parent().child().mapNested().get("default").list()); + assertEquals("value", mapping.parent().child().mapNested().get("default").map().get("default")); + } + + @Test + void emptyPrefix() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "list-nested[0].value", "value", + "parent.list-nested[0].value", "value")) + .withMapping(Defaults.class, "") + .build(); + + Defaults mapping = config.getConfigMapping(Defaults.class, ""); + + assertEquals("value", mapping.value()); + assertEquals(10, mapping.primitive()); + assertTrue(mapping.optional().isPresent()); + assertEquals("value", mapping.optional().get()); + assertTrue(mapping.optionalPrimitive().isPresent()); + assertEquals(10, mapping.optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.list()); + assertEquals("value", mapping.map().get("default")); + + assertEquals("value", mapping.nested().value()); + assertEquals(10, mapping.nested().primitive()); + assertTrue(mapping.nested().optional().isPresent()); + assertEquals("value", mapping.nested().optional().get()); + assertTrue(mapping.nested().optionalPrimitive().isPresent()); + assertEquals(10, mapping.nested().optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.nested().list()); + assertEquals("value", mapping.nested().map().get("default")); + + assertTrue(mapping.optionalNested().isPresent()); + assertEquals("value", mapping.optionalNested().get().value()); + assertEquals(10, mapping.optionalNested().get().primitive()); + assertTrue(mapping.optionalNested().get().optional().isPresent()); + assertEquals("value", mapping.optionalNested().get().optional().get()); + assertTrue(mapping.optionalNested().get().optionalPrimitive().isPresent()); + assertEquals(10, mapping.optionalNested().get().optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.optionalNested().get().list()); + assertEquals("value", mapping.optionalNested().get().map().get("default")); + + assertEquals("value", mapping.listNested().get(0).value()); + assertEquals(10, mapping.listNested().get(0).primitive()); + assertTrue(mapping.listNested().get(0).optional().isPresent()); + assertEquals("value", mapping.listNested().get(0).optional().get()); + assertTrue(mapping.listNested().get(0).optionalPrimitive().isPresent()); + assertEquals(10, mapping.listNested().get(0).optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.listNested().get(0).list()); + assertEquals("value", mapping.listNested().get(0).map().get("default")); + + assertEquals("value", mapping.mapNested().get("default").value()); + assertEquals(10, mapping.mapNested().get("default").primitive()); + assertTrue(mapping.mapNested().get("default").optional().isPresent()); + assertEquals("value", mapping.mapNested().get("default").optional().get()); + assertTrue(mapping.mapNested().get("default").optionalPrimitive().isPresent()); + assertEquals(10, mapping.mapNested().get("default").optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.mapNested().get("default").list()); + assertEquals("value", mapping.mapNested().get("default").map().get("default")); + + assertEquals("value", mapping.parent().child().nested().value()); + assertEquals(10, mapping.parent().child().nested().primitive()); + assertTrue(mapping.parent().child().nested().optional().isPresent()); + assertEquals("value", mapping.parent().child().nested().optional().get()); + assertTrue(mapping.parent().child().nested().optionalPrimitive().isPresent()); + assertEquals(10, mapping.parent().child().nested().optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.parent().child().nested().list()); + assertEquals("value", mapping.parent().child().nested().map().get("default")); + + assertTrue(mapping.parent().child().optionalNested().isPresent()); + assertEquals("value", mapping.parent().child().optionalNested().get().value()); + assertEquals(10, mapping.parent().child().optionalNested().get().primitive()); + assertTrue(mapping.parent().child().optionalNested().get().optional().isPresent()); + assertEquals("value", mapping.parent().child().optionalNested().get().optional().get()); + assertTrue(mapping.parent().child().optionalNested().get().optionalPrimitive().isPresent()); + assertEquals(10, mapping.parent().child().optionalNested().get().optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.parent().child().optionalNested().get().list()); + assertEquals("value", mapping.parent().child().optionalNested().get().map().get("default")); + + assertEquals("value", mapping.parent().child().listNested().get(0).value()); + assertEquals(10, mapping.parent().child().listNested().get(0).primitive()); + assertTrue(mapping.parent().child().listNested().get(0).optional().isPresent()); + assertEquals("value", mapping.parent().child().listNested().get(0).optional().get()); + assertTrue(mapping.parent().child().listNested().get(0).optionalPrimitive().isPresent()); + assertEquals(10, mapping.parent().child().listNested().get(0).optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.parent().child().listNested().get(0).list()); + assertEquals("value", mapping.parent().child().listNested().get(0).map().get("default")); + + assertEquals("value", mapping.parent().child().mapNested().get("default").value()); + assertEquals(10, mapping.parent().child().mapNested().get("default").primitive()); + assertTrue(mapping.parent().child().mapNested().get("default").optional().isPresent()); + assertEquals("value", mapping.parent().child().mapNested().get("default").optional().get()); + assertTrue(mapping.parent().child().mapNested().get("default").optionalPrimitive().isPresent()); + assertEquals(10, mapping.parent().child().mapNested().get("default").optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.parent().child().mapNested().get("default").list()); + assertEquals("value", mapping.parent().child().mapNested().get("default").map().get("default")); + } + + @ConfigMapping(prefix = "defaults") + interface Defaults { + @WithDefault("value") + String value(); + + @WithDefault("10") + int primitive(); + + @WithDefault("value") + Optional optional(); + + @WithDefault("10") + OptionalInt optionalPrimitive(); + + @WithDefault("one,two") + List list(); + + @WithDefault("value") + Map map(); + + Nested nested(); + + Optional optionalNested(); + + List listNested(); + + @WithDefaults + Map mapNested(); + + Parent parent(); + + interface Nested { + @WithDefault("value") + String value(); + + @WithDefault("10") + int primitive(); + + @WithDefault("value") + Optional optional(); + + @WithDefault("10") + OptionalInt optionalPrimitive(); + + @WithDefault("one,two") + List list(); + + @WithDefault("value") + Map map(); + } + + interface Parent { + @WithParentName + Child child(); + } + + interface Child { + Nested nested(); + + Optional optionalNested(); + + List listNested(); + + @WithDefaults + Map mapNested(); + } + } + + @Test + void defaultsNamingStrategy() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "defaults.listNested[0].value", "value", + "defaults.parent.listNested[0].value", "value")) + .withMapping(DefaultsWithNamingStrategy.class) + .build(); + + DefaultsWithNamingStrategy mapping = config.getConfigMapping(DefaultsWithNamingStrategy.class); + + assertEquals("value", mapping.value()); + assertEquals(10, mapping.primitive()); + assertTrue(mapping.optional().isPresent()); + assertEquals("value", mapping.optional().get()); + assertTrue(mapping.optionalPrimitive().isPresent()); + assertEquals(10, mapping.optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.list()); + assertEquals("value", mapping.map().get("default")); + + assertEquals("value", mapping.nested().value()); + assertEquals(10, mapping.nested().primitive()); + assertTrue(mapping.nested().optional().isPresent()); + assertEquals("value", mapping.nested().optional().get()); + assertTrue(mapping.nested().optionalPrimitive().isPresent()); + assertEquals(10, mapping.nested().optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.nested().list()); + assertEquals("value", mapping.nested().map().get("default")); + + assertTrue(mapping.optionalNested().isPresent()); + assertEquals("value", mapping.optionalNested().get().value()); + assertEquals(10, mapping.optionalNested().get().primitive()); + assertTrue(mapping.optionalNested().get().optional().isPresent()); + assertEquals("value", mapping.optionalNested().get().optional().get()); + assertTrue(mapping.optionalNested().get().optionalPrimitive().isPresent()); + assertEquals(10, mapping.optionalNested().get().optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.optionalNested().get().list()); + assertEquals("value", mapping.optionalNested().get().map().get("default")); + + assertEquals("value", mapping.listNested().get(0).value()); + assertEquals(10, mapping.listNested().get(0).primitive()); + assertTrue(mapping.listNested().get(0).optional().isPresent()); + assertEquals("value", mapping.listNested().get(0).optional().get()); + assertTrue(mapping.listNested().get(0).optionalPrimitive().isPresent()); + assertEquals(10, mapping.listNested().get(0).optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.listNested().get(0).list()); + assertEquals("value", mapping.listNested().get(0).map().get("default")); + + assertEquals("value", mapping.mapNested().get("default").value()); + assertEquals(10, mapping.mapNested().get("default").primitive()); + assertTrue(mapping.mapNested().get("default").optional().isPresent()); + assertEquals("value", mapping.mapNested().get("default").optional().get()); + assertTrue(mapping.mapNested().get("default").optionalPrimitive().isPresent()); + assertEquals(10, mapping.mapNested().get("default").optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.mapNested().get("default").list()); + assertEquals("value", mapping.mapNested().get("default").map().get("default")); + + assertEquals("value", mapping.parent().child().nested().value()); + assertEquals(10, mapping.parent().child().nested().primitive()); + assertTrue(mapping.parent().child().nested().optional().isPresent()); + assertEquals("value", mapping.parent().child().nested().optional().get()); + assertTrue(mapping.parent().child().nested().optionalPrimitive().isPresent()); + assertEquals(10, mapping.parent().child().nested().optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.parent().child().nested().list()); + assertEquals("value", mapping.parent().child().nested().map().get("default")); + + assertTrue(mapping.parent().child().optionalNested().isPresent()); + assertEquals("value", mapping.parent().child().optionalNested().get().value()); + assertEquals(10, mapping.parent().child().optionalNested().get().primitive()); + assertTrue(mapping.parent().child().optionalNested().get().optional().isPresent()); + assertEquals("value", mapping.parent().child().optionalNested().get().optional().get()); + assertTrue(mapping.parent().child().optionalNested().get().optionalPrimitive().isPresent()); + assertEquals(10, mapping.parent().child().optionalNested().get().optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.parent().child().optionalNested().get().list()); + assertEquals("value", mapping.parent().child().optionalNested().get().map().get("default")); + + assertEquals("value", mapping.parent().child().listNested().get(0).value()); + assertEquals(10, mapping.parent().child().listNested().get(0).primitive()); + assertTrue(mapping.parent().child().listNested().get(0).optional().isPresent()); + assertEquals("value", mapping.parent().child().listNested().get(0).optional().get()); + assertTrue(mapping.parent().child().listNested().get(0).optionalPrimitive().isPresent()); + assertEquals(10, mapping.parent().child().listNested().get(0).optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.parent().child().listNested().get(0).list()); + assertEquals("value", mapping.parent().child().listNested().get(0).map().get("default")); + + assertEquals("value", mapping.parent().child().mapNested().get("default").value()); + assertEquals(10, mapping.parent().child().mapNested().get("default").primitive()); + assertTrue(mapping.parent().child().mapNested().get("default").optional().isPresent()); + assertEquals("value", mapping.parent().child().mapNested().get("default").optional().get()); + assertTrue(mapping.parent().child().mapNested().get("default").optionalPrimitive().isPresent()); + assertEquals(10, mapping.parent().child().mapNested().get("default").optionalPrimitive().getAsInt()); + assertIterableEquals(List.of("one", "two"), mapping.parent().child().mapNested().get("default").list()); + assertEquals("value", mapping.parent().child().mapNested().get("default").map().get("default")); + } + + @ConfigMapping(prefix = "defaults", namingStrategy = ConfigMapping.NamingStrategy.VERBATIM) + interface DefaultsWithNamingStrategy { + @WithDefault("value") + String value(); + + @WithDefault("10") + int primitive(); + + @WithDefault("value") + Optional optional(); + + @WithDefault("10") + OptionalInt optionalPrimitive(); + + @WithDefault("one,two") + List list(); + + @WithDefault("value") + Map map(); + + Nested nested(); + + Optional optionalNested(); + + List listNested(); + + @WithDefaults + Map mapNested(); + + Parent parent(); + + interface Nested { + @WithDefault("value") + String value(); + + @WithDefault("10") + int primitive(); + + @WithDefault("value") + Optional optional(); + + @WithDefault("10") + OptionalInt optionalPrimitive(); + + @WithDefault("one,two") + List list(); + + @WithDefault("value") + Map map(); + } + + interface Parent { + @WithParentName + Child child(); + } + + interface Child { + Nested nested(); + + Optional optionalNested(); + + List listNested(); + + @WithDefaults + Map mapNested(); + } + } + + @Test + void defaultsParentName() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(DefaultsParentName.class) + .build(); + + // TODO - Complete + } + + @ConfigMapping(prefix = "defaults") + interface DefaultsParentName { + @WithParentName + @WithDefault("value") + String value(); + } + + @Test + void moreDefaults() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(DataSourcesJdbcBuildTimeConfig.class) + .withMapping(DataSourcesJdbcRuntimeConfig.class) + .build(); + + DataSourcesJdbcBuildTimeConfig mapping = config.getConfigMapping(DataSourcesJdbcBuildTimeConfig.class); + DataSourceJdbcOuterNamedBuildTimeConfig mapDefault = mapping.dataSources().get(""); + assertNotNull(mapDefault); + assertTrue(mapDefault.jdbc().enabled()); + assertFalse(mapDefault.jdbc().tracing()); + assertFalse(mapDefault.jdbc().telemetry()); + } + + @ConfigMapping(prefix = "quarkus.datasource") + public interface DataSourcesJdbcBuildTimeConfig { + + @WithParentName + @WithDefaults + @WithUnnamedKey("") + Map dataSources(); + + interface DataSourceJdbcOuterNamedBuildTimeConfig { + DataSourceJdbcBuildTimeConfig jdbc(); + } + + interface DataSourceJdbcBuildTimeConfig { + @WithParentName + @WithDefault("true") + boolean enabled(); + + Optional driver(); + + Optional enableMetrics(); + + @WithDefault("false") + boolean tracing(); + + @WithDefault("false") + boolean telemetry(); + } + } + + @ConfigMapping(prefix = "quarkus.datasource") + public interface DataSourcesJdbcRuntimeConfig { + + DataSourceJdbcRuntimeConfig jdbc(); + + @WithParentName + @WithDefaults + Map namedDataSources(); + + interface DataSourceJdbcOuterNamedRuntimeConfig { + DataSourceJdbcRuntimeConfig jdbc(); + } + + interface DataSourceJdbcRuntimeConfig { + @WithDefault("0") + int minSize(); + + @WithDefault("20") + int maxSize(); + + @WithDefault("2M") + String backgroundValidationInterval(); + + @WithDefault("5S") + Optional acquisitionTimeout(); + + @WithDefault("5M") + String idleRemovalInterval(); + + @WithDefault("false") + boolean extendedLeakReport(); + + @WithDefault("false") + boolean flushOnClose(); + + @WithDefault("true") + boolean detectStatementLeaks(); + + @WithDefault("true") + boolean poolingEnabled(); + + DataSourceJdbcTracingRuntimeConfig tracing(); + + interface DataSourceJdbcTracingRuntimeConfig { + @WithDefault("false") + boolean traceWithActiveSpanOnly(); + } + } + } + + @Test + void parentDefaults() { + Map defaults = getDefaults(configClassWithPrefix(ExtendsBase.class)); + assertEquals(2, defaults.size()); + assertEquals("default", defaults.get("base.base")); + assertEquals("default", defaults.get("base.my-prop")); + } + + public interface Base { + @WithDefault("default") + String base(); + } + + @ConfigMapping(prefix = "base") + public interface ExtendsBase extends Base { + @WithDefault("default") + String myProp(); + } + + @Test + void mapDefaults() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(MapDefaults.class) + .build(); + + MapDefaults mapping = config.getConfigMapping(MapDefaults.class); + assertIterableEquals(List.of("foo", "bar"), mapping.map().get("any")); + + config = new SmallRyeConfigBuilder() + .withSources(config("map.defaults.any", "one,two")) + .withMapping(MapDefaults.class) + .build(); + + mapping = config.getConfigMapping(MapDefaults.class); + assertIterableEquals(List.of("one", "two"), mapping.map().get("any")); + + config = new SmallRyeConfigBuilder() + .withSources(config("map.defaults.any[0]", "one", "map.defaults.any[1]", "two")) + .withMapping(MapDefaults.class) + .build(); + + mapping = config.getConfigMapping(MapDefaults.class); + assertIterableEquals(List.of("one", "two"), mapping.map().get("any")); + } + + @ConfigMapping(prefix = "map.defaults") + interface MapDefaults { + @WithParentName + @WithDefault("foo,bar") + Map> map(); + } + + @Test + void groupParentDefaults() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(S3BuildTimeConfig.class) + .build(); + + S3BuildTimeConfig mapping = config.getConfigMapping(S3BuildTimeConfig.class); + assertEquals(URL, mapping.syncClient().type()); + assertEquals(NETTY, mapping.asyncClient().type()); + assertIterableEquals(Set.of("default"), mapping.devservices().buckets()); + assertFalse(mapping.devservices().shared()); + assertEquals("localstack", mapping.devservices().serviceName()); + } + + @ConfigMapping(prefix = "quarkus.s3") + public interface S3BuildTimeConfig extends HasSdkBuildTimeConfig { + SyncHttpClientBuildTimeConfig syncClient(); + + AsyncHttpClientBuildTimeConfig asyncClient(); + + S3DevServicesBuildTimeConfig devservices(); + + interface SyncHttpClientBuildTimeConfig { + @WithDefault(value = "url") + SyncClientType type(); + + enum SyncClientType { + URL, + APACHE + } + } + + interface AsyncHttpClientBuildTimeConfig { + @WithDefault(value = "netty") + AsyncClientType type(); + + enum AsyncClientType { + NETTY, + AWS_CRT + } + } + + interface S3DevServicesBuildTimeConfig extends DevServicesBuildTimeConfig { + @WithDefault(value = "default") + Set buckets(); + } + } + + public interface HasSdkBuildTimeConfig { + @WithParentName + SdkBuildTimeConfig sdk(); + } + + public interface SdkBuildTimeConfig { + Optional> interceptors(); + } + + public interface DevServicesBuildTimeConfig { + Optional enabled(); + + @WithDefault(value = "false") + boolean shared(); + + @WithDefault(value = "localstack") + String serviceName(); + + Map containerProperties(); + } + + @Test + void multipleLevelDefaults() { + Map defaults = getDefaults(configClassWithPrefix(GrandChild.class)); + assertEquals("parent", defaults.get("parent")); + assertEquals("child", defaults.get("child")); + assertEquals("grand-child", defaults.get("grand-child")); + assertEquals("child", defaults.get("override-by-child")); + assertEquals("grand-child", defaults.get("override-by-grand-child")); + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(GrandChild.class) + .build(); + + GrandChild mapping = config.getConfigMapping(GrandChild.class); + assertEquals("parent", mapping.parent()); + assertEquals("child", mapping.child()); + assertEquals("grand-child", mapping.grandChild()); + assertEquals("child", mapping.overrideByChild()); + assertEquals("grand-child", mapping.overrideByGrandChild()); + } + + interface Parent { + @WithDefault("parent") + String parent(); + + @WithDefault("parent") + String overrideByChild(); + + @WithDefault("parent") + String overrideByGrandChild(); + } + + interface Child extends Parent { + @WithDefault("child") + String child(); + + @WithDefault("child") + String overrideByChild(); + + @WithDefault("child") + String overrideByGrandChild(); + } + + @ConfigMapping + interface GrandChild extends Child { + @WithDefault("grand-child") + String grandChild(); + + @WithDefault("grand-child") + String overrideByGrandChild(); + } +} diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingFullTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingFullTest.java new file mode 100644 index 000000000..c10010921 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingFullTest.java @@ -0,0 +1,237 @@ +package io.smallrye.config; + +import static io.smallrye.config.KeyValuesConfigSource.config; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +import org.junit.jupiter.api.Test; + +public class ConfigMappingFullTest { + @Test + void ambiguousUnnamedKeysDefaults() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "datasource.postgresql.jdbc.url", "value", + "datasource.postgresql.password", "value")) + .withMapping(DataSourcesJdbcRuntimeConfig.class) + .withMapping(DataSourcesJdbcBuildTimeConfig.class) + .withMapping(DataSourcesRuntimeConfig.class) + .withMapping(DataSourcesBuildTimeConfig.class) + .build(); + + assertTrue( + config.getConfigMapping(DataSourcesRuntimeConfig.class).dataSources().get("postgresql").password().isPresent()); + + config = new SmallRyeConfigBuilder() + .withSources(config( + "datasource.postgresql.jdbc.url", "value", + "datasource.postgresql.password", "value")) + .withMapping(DataSourcesJdbcBuildTimeConfig.class) + .withMapping(DataSourcesJdbcRuntimeConfig.class) + .withMapping(DataSourcesRuntimeConfig.class) + .withMapping(DataSourcesBuildTimeConfig.class) + .build(); + + assertTrue( + config.getConfigMapping(DataSourcesRuntimeConfig.class).dataSources().get("postgresql").password().isPresent()); + + config = new SmallRyeConfigBuilder() + .withSources(config( + "datasource.postgresql.jdbc.url", "value", + "datasource.postgresql.password", "value")) + .withMapping(DataSourcesJdbcBuildTimeConfig.class) + .withMapping(DataSourcesJdbcRuntimeConfig.class) + .withMapping(DataSourcesBuildTimeConfig.class) + .withMapping(DataSourcesRuntimeConfig.class) + .build(); + + assertTrue( + config.getConfigMapping(DataSourcesRuntimeConfig.class).dataSources().get("postgresql").password().isPresent()); + + config = new SmallRyeConfigBuilder() + .withSources(config( + "datasource.postgresql.jdbc.url", "value", + "datasource.postgresql.password", "value")) + .withMapping(DataSourcesRuntimeConfig.class) + .withMapping(DataSourcesJdbcBuildTimeConfig.class) + .withMapping(DataSourcesJdbcRuntimeConfig.class) + .withMapping(DataSourcesBuildTimeConfig.class) + .build(); + + assertTrue( + config.getConfigMapping(DataSourcesRuntimeConfig.class).dataSources().get("postgresql").password().isPresent()); + } + + @ConfigMapping(prefix = "datasource") + interface DataSourcesRuntimeConfig { + @WithParentName + @WithDefaults + @WithUnnamedKey("") + Map dataSources(); + + interface DataSourceRuntimeConfig { + @WithDefault("true") + boolean active(); + + Optional username(); + + Optional password(); + + Optional credentialsProvider(); + + Optional credentialsProviderName(); + } + } + + @ConfigMapping(prefix = "datasource") + interface DataSourcesJdbcRuntimeConfig { + DataSourceJdbcRuntimeConfig jdbc(); + + @WithParentName + @WithDefaults + Map namedDataSources(); + + interface DataSourceJdbcOuterNamedRuntimeConfig { + DataSourceJdbcRuntimeConfig jdbc(); + } + + interface DataSourceJdbcRuntimeConfig { + Optional url(); + + OptionalInt initialSize(); + + @WithDefault("0") + int minSize(); + + @WithDefault("20") + int maxSize(); + + @WithDefault("2M") + String backgroundValidationInterval(); + + Optional foregroundValidationInterval(); + + @WithDefault("5S") + Optional acquisitionTimeout(); + + Optional leakDetectionInterval(); + + @WithDefault("5M") + String idleRemovalInterval(); + + Optional maxLifetime(); + + @WithDefault("false") + boolean extendedLeakReport(); + + @WithDefault("false") + boolean flushOnClose(); + + @WithDefault("true") + boolean detectStatementLeaks(); + + Optional newConnectionSql(); + + Optional validationQuerySql(); + + @WithDefault("true") + boolean poolingEnabled(); + + Map additionalJdbcProperties(); + + @WithName("telemetry.enabled") + Optional telemetry(); + } + } + + @ConfigMapping(prefix = "datasource") + interface DataSourcesBuildTimeConfig { + @WithParentName + @WithDefaults + @WithUnnamedKey("") + Map dataSources(); + + @WithName("health.enabled") + @WithDefault("true") + boolean healthEnabled(); + + @WithName("metrics.enabled") + @WithDefault("false") + boolean metricsEnabled(); + + Optional url(); + + Optional driver(); + + interface DataSourceBuildTimeConfig { + Optional dbKind(); + + Optional dbVersion(); + + DevServicesBuildTimeConfig devservices(); + + @WithDefault("false") + boolean healthExclude(); + + interface DevServicesBuildTimeConfig { + Optional enabled(); + + Optional imageName(); + + Map containerEnv(); + + Map containerProperties(); + + Map properties(); + + OptionalInt port(); + + Optional command(); + + Optional dbName(); + + Optional username(); + + Optional password(); + + Optional initScriptPath(); + + Map volumes(); + + @WithDefault("true") + boolean reuse(); + } + } + } + + @ConfigMapping(prefix = "datasource") + interface DataSourcesJdbcBuildTimeConfig { + @WithParentName + @WithDefaults + @WithUnnamedKey("") + Map dataSources(); + + interface DataSourceJdbcOuterNamedBuildTimeConfig { + DataSourceJdbcBuildTimeConfig jdbc(); + + interface DataSourceJdbcBuildTimeConfig { + @WithParentName + @WithDefault("true") + boolean enabled(); + + Optional driver(); + + Optional enableMetrics(); + + @WithDefault("false") + boolean tracing(); + + @WithDefault("false") + boolean telemetry(); + } + } + } +} diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingInterfaceTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingInterfaceTest.java index cdab95343..ea8492a50 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingInterfaceTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingInterfaceTest.java @@ -1,23 +1,30 @@ package io.smallrye.config; +import static io.smallrye.config.ConfigMapping.NamingStrategy.VERBATIM; import static io.smallrye.config.ConfigMappingInterfaceTest.HyphenatedEnumMapping.HyphenatedEnum.VALUE_ONE; import static io.smallrye.config.ConfigMappingInterfaceTest.MapKeyEnum.ClientId.NAF; import static io.smallrye.config.ConfigMappingInterfaceTest.MapKeyEnum.ClientId.SOS_DAH; import static io.smallrye.config.KeyValuesConfigSource.config; +import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; import static java.util.stream.StreamSupport.stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.LocalDateTime; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -31,43 +38,44 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.smallrye.config.ConfigMappingInterfaceTest.MyRestClientConfig.RestClientConfig; import io.smallrye.config.common.MapBackedConfigSource; class ConfigMappingInterfaceTest { @Test void configMapping() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(Server.class, "server") .withSources(config("server.host", "localhost", "server.port", "8080")).build(); - final Server configProperties = config.getConfigMapping(Server.class, "server"); + Server configProperties = config.getConfigMapping(Server.class, "server"); assertEquals("localhost", configProperties.host()); assertEquals(8080, configProperties.port()); } @Test void noConfigMapping() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withSources(config("server.host", "localhost", "server.port", "8080")).build(); - final NoSuchElementException exception = assertThrows(NoSuchElementException.class, + NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> config.getConfigMapping(Server.class, "server")); assertEquals("SRCFG00027: Could not find a mapping for " + Server.class.getName(), exception.getMessage()); } @Test void unregisteredConfigMapping() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withSources(config("host", "localhost", "port", "8080")).build(); - final NoSuchElementException exception = assertThrows(NoSuchElementException.class, + NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> config.getConfigMapping(Server.class)); assertEquals("SRCFG00027: Could not find a mapping for " + Server.class.getName(), exception.getMessage()); } @Test void unregistedPrefix() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(Server.class) .withSources(config("host", "localhost", "port", "8080")).build(); - final NoSuchElementException exception = assertThrows(NoSuchElementException.class, + NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> config.getConfigMapping(Server.class, "server")); assertEquals("SRCFG00028: Could not find a mapping for " + Server.class.getName() + " with prefix server", exception.getMessage()); @@ -75,40 +83,27 @@ void unregistedPrefix() { @Test void noPrefix() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(Server.class) .withSources(config("host", "localhost", "port", "8080")).build(); - final Server configProperties = config.getConfigMapping(Server.class); + Server configProperties = config.getConfigMapping(Server.class); assertEquals("localhost", configProperties.host()); assertEquals(8080, configProperties.port()); } - @Test - void configMappingBuilder() { - final ConfigMappingProvider configMappingProvider = ConfigMappingProvider.builder().addRoot("server", Server.class) - .addIgnored("server.name").build(); - final SmallRyeConfig config = new SmallRyeConfigBuilder().withSources( - config("server.host", "localhost", "server.port", "8080", "server.name", "name")).build(); - - configMappingProvider.mapConfiguration(config); - final Server server = config.getConfigMapping(Server.class, "server"); - assertEquals("localhost", server.host()); - assertEquals(8080, server.port()); - } - @Test void unknownConfigElement() { - assertThrows(IllegalStateException.class, + assertThrows(ConfigValidationException.class, () -> new SmallRyeConfigBuilder().withMapping(Server.class, "server").build()); } @Test void ignorePropertiesInUnregisteredRoots() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(Server.class, "server") .withSources(config("server.host", "localhost", "server.port", "8080", "client.name", "konoha")) .build(); - final Server configProperties = config.getConfigMapping(Server.class, "server"); + Server configProperties = config.getConfigMapping(Server.class, "server"); assertEquals("localhost", configProperties.host()); assertEquals(8080, configProperties.port()); } @@ -123,32 +118,32 @@ void ignoreSomeProperties() { "client.port", "8080", "client.name", "konoha")) .build(); - final Server server = config.getConfigMapping(Server.class, "server"); + Server server = config.getConfigMapping(Server.class, "server"); assertEquals("localhost", server.host()); assertEquals(8080, server.port()); - final Client client = config.getConfigMapping(Client.class, "client"); + Client client = config.getConfigMapping(Client.class, "client"); assertEquals("localhost", client.host()); assertEquals(8080, client.port()); } @Test void ignoreProperties() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .addDefaultSources() .withMapping(Server.class, "server") .withSources(config("server.host", "localhost", "server.port", "8080")).build(); - final Server configProperties = config.getConfigMapping(Server.class, "server"); + Server configProperties = config.getConfigMapping(Server.class, "server"); assertEquals("localhost", configProperties.host()); assertEquals(8080, configProperties.port()); } @Test void validateUnknown() { - assertThrows(IllegalStateException.class, + assertThrows(ConfigValidationException.class, () -> new SmallRyeConfigBuilder().addDefaultSources().withMapping(Server.class).build()); - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .addDefaultSources() .withMapping(Server.class) .withMapping(Server.class, "server") @@ -156,53 +151,48 @@ void validateUnknown() { .withSources(config("server.host", "localhost", "server.port", "8080", "host", "localhost", "port", "8080")) .build(); - final Server configProperties = config.getConfigMapping(Server.class); + Server configProperties = config.getConfigMapping(Server.class); assertEquals("localhost", configProperties.host()); assertEquals(8080, configProperties.port()); } @Test void splitRoots() { - final SmallRyeConfig config = new SmallRyeConfigBuilder().withSources( - config("server.host", "localhost", "server.port", "8080", "server.name", "konoha")) - .build(); - - final ConfigMappingProvider configMappingProvider = ConfigMappingProvider.builder() - .addRoot("server", SplitRootServerHostAndPort.class) - .addRoot("server", SplitRootServerName.class) + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("server.host", "localhost", "server.port", "8080", "server.name", "konoha")) + .withMapping(SplitRootServerHostAndPort.class, "server") + .withMapping(SplitRootServerName.class, "server") .build(); - configMappingProvider.mapConfiguration(config); - - final SplitRootServerHostAndPort server = config.getConfigMapping(SplitRootServerHostAndPort.class, "server"); + SplitRootServerHostAndPort server = config.getConfigMapping(SplitRootServerHostAndPort.class, "server"); assertEquals("localhost", server.host()); assertEquals(8080, server.port()); - final SplitRootServerName name = config.getConfigMapping(SplitRootServerName.class, "server"); + SplitRootServerName name = config.getConfigMapping(SplitRootServerName.class, "server"); assertEquals("konoha", name.name()); } @Test void splitRootsInConfig() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withSources(config("server.host", "localhost", "server.port", "8080", "server.name", "konoha")) .withMapping(SplitRootServerHostAndPort.class, "server") .withMapping(SplitRootServerName.class, "server") .build(); - final SplitRootServerHostAndPort server = config.getConfigMapping(SplitRootServerHostAndPort.class, "server"); + SplitRootServerHostAndPort server = config.getConfigMapping(SplitRootServerHostAndPort.class, "server"); assertEquals("localhost", server.host()); assertEquals(8080, server.port()); } @Test void subGroups() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withSources(config("server.host", "localhost", "server.port", "8080", "server.name", "konoha")) .withMapping(ServerSub.class, "server") .build(); - final ServerSub server = config.getConfigMapping(ServerSub.class, "server"); + ServerSub server = config.getConfigMapping(ServerSub.class, "server"); assertEquals("localhost", server.subHostAndPort().host()); assertEquals(8080, server.subHostAndPort().port()); assertEquals("konoha", server.subName().name()); @@ -210,22 +200,17 @@ void subGroups() { @Test void types() { - final Map typesConfig = new HashMap() { - { - put("int", "9"); - put("long", "9999999999"); - put("float", "99.9"); - put("double", "99.99"); - put("char", "c"); - put("boolean", "true"); - } - }; - - final SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(config(typesConfig)) + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "int", "9", + "long", "9999999999", + "float", "99.9", + "double", "99.99", + "char", "c", + "boolean", "true")) .withMapping(SomeTypes.class) .build(); - final SomeTypes types = config.getConfigMapping(SomeTypes.class); + SomeTypes types = config.getConfigMapping(SomeTypes.class); assertEquals(9, types.intPrimitive()); assertEquals(9, types.intWrapper()); @@ -243,23 +228,18 @@ void types() { @Test void optionals() { - final Map typesConfig = new HashMap() { - { - put("server.host", "localhost"); - put("server.port", "8080"); - put("optional", "optional"); - put("optional.int", "9"); - put("info.name", "server"); - put("info.login", "login"); - put("info.password", "password"); - } - }; - - final SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(config(typesConfig)) + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "server.host", "localhost", + "server.port", "8080", + "optional", "optional", + "optional.int", "9", + "info.name", "server", + "info.login", "login", + "info.password", "password")) .withMapping(Optionals.class) .build(); - final Optionals optionals = config.getConfigMapping(Optionals.class); + Optionals optionals = config.getConfigMapping(Optionals.class); assertTrue(optionals.server().isPresent()); assertEquals("localhost", optionals.server().get().host()); @@ -270,7 +250,7 @@ void optionals() { assertTrue(optionals.optionalInt().isPresent()); assertEquals(9, optionals.optionalInt().getAsInt()); - assertFalse(optionals.info().isEmpty()); + assertEquals(1, optionals.info().size()); assertEquals("server", optionals.info().get("info").name()); assertTrue(optionals.info().get("info").login().isPresent()); assertEquals("login", optionals.info().get("info").login().get()); @@ -280,18 +260,11 @@ void optionals() { @Test void collectionTypes() { - final Map typesConfig = new HashMap() { - { - put("strings", "foo,bar"); - put("ints", "1,2,3"); - } - }; - - final SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(config(typesConfig)) + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("strings", "foo,bar", "ints", "1,2,3")) .withMapping(CollectionTypes.class) .build(); - final CollectionTypes types = config.getConfigMapping(CollectionTypes.class); + CollectionTypes types = config.getConfigMapping(CollectionTypes.class); assertEquals(Stream.of("foo", "bar").collect(toList()), types.listStrings()); assertEquals(Stream.of(1, 2, 3).collect(toList()), types.listInts()); @@ -299,111 +272,90 @@ void collectionTypes() { @Test void maps() { - final Map typesConfig = new HashMap() { - { - put("server.host", "localhost"); - put("server.port", "8080"); - - put("server.server.host", "localhost-server"); - put("server.server.port", "8080"); - - put("server.group.server.host", "localhost-group"); - put("server.group.server.port", "8080"); - } - }; - - final SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(config(typesConfig)) + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "server.host", "localhost", + "server.port", "8080", + "server.group.server.host", "localhost-group", + "server.group.server.port", "8081", + "server.server.host", "localhost-server", + "server.server.port", "8082")) .withMapping(Maps.class) .build(); - final Maps maps = config.getConfigMapping(Maps.class); + Maps maps = config.getConfigMapping(Maps.class); + assertEquals(6, maps.server().size()); assertEquals("localhost", maps.server().get("host")); assertEquals(8080, Integer.valueOf(maps.server().get("port"))); + assertEquals(1, maps.group().size()); assertEquals("localhost-group", maps.group().get("server").host()); - assertEquals(8080, maps.group().get("server").port()); + assertEquals(8081, maps.group().get("server").port()); + assertEquals(1, maps.groupParentName().size()); assertEquals("localhost-server", maps.groupParentName().get("server").host()); - assertEquals(8080, maps.groupParentName().get("server").port()); + assertEquals(8082, maps.groupParentName().get("server").port()); } @Test void mapsEmptyPrefix() { - final Map typesConfig = new HashMap() { - { - put("host", "localhost"); - put("port", "8080"); - - put("server.host", "localhost"); - put("server.port", "8080"); - - put("group.server.host", "localhost"); - put("group.server.port", "8080"); - } - }; - - final SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(config(typesConfig)) + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "host", "localhost", + "port", "8080", + "group.server.host", "localhost", + "group.server.port", "8081", + "server.host", "localhost", + "server.port", "8082")) .withMapping(Maps.class, "") .build(); - final Maps maps = config.getConfigMapping(Maps.class, ""); + Maps maps = config.getConfigMapping(Maps.class, ""); + assertEquals(6, maps.server().size()); assertEquals("localhost", maps.server().get("host")); assertEquals(8080, Integer.valueOf(maps.server().get("port"))); + assertEquals(1, maps.group().size()); assertEquals("localhost", maps.group().get("server").host()); - assertEquals(8080, maps.group().get("server").port()); + assertEquals(8081, maps.group().get("server").port()); + assertEquals(1, maps.groupParentName().size()); assertEquals("localhost", maps.groupParentName().get("server").host()); - assertEquals(8080, maps.groupParentName().get("server").port()); - } - - @Test - void defaults() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() - .withMapping(Defaults.class) - .build(); - final Defaults defaults = config.getConfigMapping(Defaults.class); - - assertEquals("foo", defaults.foo()); - assertEquals("bar", defaults.bar()); - assertEquals("foo", config.getRawValue("foo")); - - final List propertyNames = stream(config.getPropertyNames().spliterator(), false).collect(toList()); - assertFalse(propertyNames.contains("foo")); + assertEquals(8082, maps.groupParentName().get("server").port()); } @Test void converters() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withSources(config("foo", "notbar")) .withMapping(Converters.class) .build(); - final Converters converters = config.getConfigMapping(Converters.class); + Converters converters = config.getConfigMapping(Converters.class); assertEquals("bar", converters.foo()); + assertTrue(converters.bprim()); + assertEquals('c', converters.cprim()); + assertEquals(-1, converters.iprim()); + assertEquals(-1, converters.sprim()); + assertEquals(-1L, converters.lprim()); + assertEquals(-1.0, converters.fprim()); + assertEquals(-1.0, converters.dprim()); } @Test void mix() { - final Map typesConfig = new HashMap() { - { - put("server.host", "localhost"); - put("server.port", "8080"); - put("server.name", "server"); - put("client.host", "clienthost"); - put("client.port", "80"); - put("client.name", "client"); - } - }; - - final SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(config(typesConfig)) + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "server.host", "localhost", + "server.port", "8080", + "server.name", "server", + "client.host", "clienthost", + "client.port", "80", + "client.name", "client")) .withMapping(ComplexSample.class) .build(); - final ComplexSample sample = config.getConfigMapping(ComplexSample.class); + ComplexSample sample = config.getConfigMapping(ComplexSample.class); assertEquals("localhost", sample.server().subHostAndPort().host()); assertEquals(8080, sample.server().subHostAndPort().port()); assertTrue(sample.client().isPresent()); @@ -413,19 +365,19 @@ void mix() { @Test void noDynamicValues() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(Server.class, "server") .withSources(config("server.host", "localhost", "server.port", "8080")) .withSources(new MapBackedConfigSource("test", new HashMap<>(), Integer.MAX_VALUE) { private int counter = 1; @Override - public String getValue(final String propertyName) { + public String getValue(String propertyName) { return counter++ + ""; } }).build(); - final Server server = config.getConfigMapping(Server.class, "server"); + Server server = config.getConfigMapping(Server.class, "server"); assertNotEquals(config.getRawValue("server.port"), config.getRawValue("server.port")); assertEquals(server.port(), server.port()); @@ -433,10 +385,10 @@ public String getValue(final String propertyName) { @Test void mapClass() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(ServerClass.class, "server") .withSources(config("server.host", "localhost", "server.port", "8080")).build(); - final ServerClass server = config.getConfigMapping(ServerClass.class, "server"); + ServerClass server = config.getConfigMapping(ServerClass.class, "server"); assertEquals("localhost", server.getHost()); assertEquals(8080, server.getPort()); } @@ -445,35 +397,35 @@ static class ServerClass { String host; int port; - public String getHost() { + String getHost() { return host; } - public int getPort() { + int getPort() { return port; } } @Test void configMappingAnnotation() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(ServerAnnotated.class, "server") .withMapping(ServerAnnotated.class, "cloud") .withSources( config("server.host", "localhost", "server.port", "8080", "cloud.host", "cloud", "cloud.port", "9090")) .build(); - final ServerAnnotated server = config.getConfigMapping(ServerAnnotated.class, "server"); + ServerAnnotated server = config.getConfigMapping(ServerAnnotated.class, "server"); assertNotNull(server); assertEquals("localhost", server.host()); assertEquals(8080, server.port()); - final ServerAnnotated cloud = config.getConfigMapping(ServerAnnotated.class); + ServerAnnotated cloud = config.getConfigMapping(ServerAnnotated.class); assertNotNull(cloud); assertEquals("cloud", cloud.host()); assertEquals(9090, cloud.port()); - final ServerAnnotated cloudNull = config.getConfigMapping(ServerAnnotated.class, null); + ServerAnnotated cloudNull = config.getConfigMapping(ServerAnnotated.class, null); assertNotNull(cloudNull); assertEquals("cloud", cloudNull.host()); assertEquals(9090, cloudNull.port()); @@ -481,12 +433,12 @@ void configMappingAnnotation() { @Test void prefixFromAnnotation() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(ServerAnnotated.class) .withSources(config("cloud.host", "cloud", "cloud.port", "9090")) .build(); - final ServerAnnotated cloud = config.getConfigMapping(ServerAnnotated.class); + ServerAnnotated cloud = config.getConfigMapping(ServerAnnotated.class); assertNotNull(cloud); assertEquals("cloud", cloud.host()); assertEquals(9090, cloud.port()); @@ -494,12 +446,12 @@ void prefixFromAnnotation() { @Test void superTypes() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(ServerChild.class, "server") .withSources(config("server.host", "localhost", "server.port", "8080")) .build(); - final ServerChild server = config.getConfigMapping(ServerChild.class, "server"); + ServerChild server = config.getConfigMapping(ServerChild.class, "server"); assertNotNull(server); assertEquals("localhost", server.host()); assertEquals(8080, server.port()); @@ -507,12 +459,12 @@ void superTypes() { @Test void configValue() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(ServerConfigValue.class, "server") .withSources(config("server.host", "localhost", "server.port", "8080")) .build(); - final ServerConfigValue server = config.getConfigMapping(ServerConfigValue.class, "server"); + ServerConfigValue server = config.getConfigMapping(ServerConfigValue.class, "server"); assertNotNull(server); assertEquals("localhost", server.host().getValue()); assertEquals(8080, Integer.valueOf(server.port().getValue())); @@ -567,7 +519,7 @@ interface ServerSubName { String name(); } - public interface SomeTypes { + interface SomeTypes { @WithName("int") int intPrimitive(); @@ -605,7 +557,7 @@ public interface SomeTypes { Boolean booleanWrapper(); } - public interface Optionals { + interface Optionals { Optional server(); Optional optional(); @@ -625,7 +577,7 @@ interface Info { } } - public interface CollectionTypes { + interface CollectionTypes { @WithName("strings") List listStrings(); @@ -634,7 +586,7 @@ public interface CollectionTypes { } @ConfigMapping(prefix = "server") - public interface Maps { + interface Maps { @WithParentName Map server(); @@ -644,14 +596,6 @@ public interface Maps { Map groupParentName(); } - public interface Defaults { - @WithDefault("foo") - String foo(); - - @WithDefault("bar") - String bar(); - } - public interface ComplexSample { ServerSub server(); @@ -661,6 +605,27 @@ public interface ComplexSample { public interface Converters { @WithConverter(FooBarConverter.class) String foo(); + + @WithConverter(BooleanConverter.class) + boolean bprim(); + + @WithConverter(CharacterConverter.class) + char cprim(); + + @WithConverter(IntegerConverter.class) + int iprim(); + + @WithConverter(ShortConverter.class) + short sprim(); + + @WithConverter(LongConverter.class) + long lprim(); + + @WithConverter(FloatConverter.class) + float fprim(); + + @WithConverter(DoubleConverter.class) + double dprim(); } public static class FooBarConverter implements Converter { @@ -670,35 +635,84 @@ public String convert(final String value) { } } + public static class BooleanConverter implements Converter { + @Override + public Boolean convert(String value) throws IllegalArgumentException, NullPointerException { + return true; + } + } + + public static class CharacterConverter implements Converter { + @Override + public Character convert(String value) throws IllegalArgumentException, NullPointerException { + return 'c'; + } + } + + public static class IntegerConverter implements Converter { + @Override + public Integer convert(String value) throws IllegalArgumentException, NullPointerException { + return -1; + } + } + + public static class ShortConverter implements Converter { + @Override + public Short convert(String value) throws IllegalArgumentException, NullPointerException { + return -1; + } + } + + public static class LongConverter implements Converter { + @Override + public Long convert(String value) throws IllegalArgumentException, NullPointerException { + return -1L; + } + } + + public static class FloatConverter implements Converter { + @Override + public Float convert(String value) throws IllegalArgumentException, NullPointerException { + return -1.0F; + } + } + + public static class DoubleConverter implements Converter { + @Override + public Double convert(String value) throws IllegalArgumentException, NullPointerException { + return -1.0; + } + } + @ConfigMapping(prefix = "cloud") - public interface ServerAnnotated { + interface ServerAnnotated { String host(); int port(); } - public interface ServerParent { + interface ServerParent { String host(); } - public interface ServerChild extends ServerParent { + interface ServerChild extends ServerParent { int port(); } @ConfigMapping(prefix = "server") - public interface ServerConfigValue { + interface ServerConfigValue { ConfigValue host(); ConfigValue port(); } @ConfigMapping(prefix = "empty") - public interface Empty { + interface Empty { } @ConfigMapping(prefix = "server") - public interface MapsInGroup { + interface MapsInGroup { Info info(); interface Info { @@ -716,20 +730,15 @@ interface Data { @Test void mapsInGroup() { - final Map typesConfig = new HashMap() { - { - put("server.info.name", "naruto"); - put("server.info.values.name", "naruto"); - put("server.info.data.first.name", "naruto"); - } - }; - - final SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(config(typesConfig)) + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "server.info.name", "naruto", + "server.info.values.name", "naruto", + "server.info.data.first.name", "naruto")) .withMapping(MapsInGroup.class) .build(); - final MapsInGroup mapping = config.getConfigMapping(MapsInGroup.class); + MapsInGroup mapping = config.getConfigMapping(MapsInGroup.class); assertNotNull(mapping); assertEquals("naruto", mapping.info().name()); assertEquals("naruto", mapping.info().values().get("name")); @@ -737,20 +746,20 @@ void mapsInGroup() { } @ConfigMapping(prefix = "server") - public interface ServerPrefix { + interface ServerPrefix { String host(); int port(); } @ConfigMapping(prefix = "server") - public interface ServerNamePrefix { + interface ServerNamePrefix { String host(); } @Test void prefixPropertyInRoot() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(ServerPrefix.class, "server") .withMapping(ServerPrefix.class, "cloud.server") .withMapping(ServerNamePrefix.class, "server") @@ -781,12 +790,12 @@ void prefixPropertyInRootUnknown() { .withMapping(ServerPrefix.class, "server") .withSources(config("serverBoot", "server")) .withSources(config("server.host", "localhost", "server.port", "8080")) - .withSources(config("server.name", "localhost")); + .withSources(config("server.name", "localhost")) + .withSources(new EnvConfigSource(Map.of("SERVER_ALIAS", "alias"), 300)); - IllegalStateException exception = assertThrows(IllegalStateException.class, builder::build); - assertTrue(exception.getCause() instanceof ConfigValidationException); - assertEquals("server.name does not map to any root", - ((ConfigValidationException) exception.getCause()).getProblem(0).getMessage()); + ConfigValidationException exception = assertThrows(ConfigValidationException.class, builder::build); + assertEquals("SRCFG00050: server.name in KeyValuesConfigSource does not map to any root", + exception.getProblem(0).getMessage()); builder = new SmallRyeConfigBuilder() .withMapping(ServerPrefix.class, "server") @@ -798,14 +807,13 @@ void prefixPropertyInRootUnknown() { .withSources(config("cloud.server.host", "localhost", "cloud.server.port", "8080")) .withSources(config("cloud.server.name", "localhost")); - exception = assertThrows(IllegalStateException.class, builder::build); - assertTrue(exception.getCause() instanceof ConfigValidationException); - assertEquals("cloud.server.name does not map to any root", - ((ConfigValidationException) exception.getCause()).getProblem(0).getMessage()); + exception = assertThrows(ConfigValidationException.class, builder::build); + assertEquals("SRCFG00050: cloud.server.name in KeyValuesConfigSource does not map to any root", + exception.getProblem(0).getMessage()); } @ConfigMapping(prefix = "mapping.server.env") - public interface ServerMapEnv { + interface ServerMapEnv { @WithParentName Map info(); @@ -821,7 +829,7 @@ interface Info { @Test void mapEnv() { SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(new EnvConfigSource(new HashMap() { + .withSources(new EnvConfigSource(new HashMap<>() { { put("MAPPING_SERVER_ENV__LOCALHOST__NAME", "development"); put("MAPPING_SERVER_ENV__LOCALHOST__ALIAS", "dev"); @@ -845,7 +853,7 @@ void mapEnv() { } @ConfigMapping(prefix = "server") - public interface ServerOptionalWithName { + interface ServerOptionalWithName { @WithName("a_server") Optional aServer(); @@ -873,7 +881,7 @@ void optionalWithName() { } @ConfigMapping(prefix = "server") - public interface ServerHierarchy extends Server { + interface ServerHierarchy extends Server { @WithParentName Named named(); @@ -906,7 +914,7 @@ void hierarchy() { } @ConfigMapping(prefix = "server") - public interface ServerExpandDefaults { + interface ServerExpandDefaults { @WithDefault("localhost") String host(); @@ -1056,7 +1064,7 @@ void path() { } @ConfigMapping(prefix = "map") - public interface DottedKeyInMap { + interface DottedKeyInMap { @WithParentName Map map(); } @@ -1075,7 +1083,7 @@ void properties() { // From https://github.com/quarkusio/quarkus/issues/20728 @ConfigMapping(prefix = "clients") - public interface BugsConfiguration { + interface BugsConfiguration { @WithParentName Map clients(); @@ -1126,7 +1134,7 @@ void mapWithMultipleGroupsAndSameMethodNames() { } @ConfigMapping(prefix = "defaults") - public interface DefaultMethods { + interface DefaultMethods { default String host() { return "localhost"; } @@ -1147,7 +1155,7 @@ void defaultMethods() { } @ConfigMapping(prefix = "defaults") - public interface DefaultKotlinMethods { + interface DefaultKotlinMethods { String host(); @WithDefault("8080") @@ -1198,7 +1206,7 @@ void defaultKotlinMethods() { } @ConfigMapping(prefix = "clients", namingStrategy = ConfigMapping.NamingStrategy.VERBATIM) - public interface MapKeyEnum { + interface MapKeyEnum { enum ClientId { SOS_DAH, NAF @@ -1287,16 +1295,16 @@ class Range { private final Integer min; private final Integer max; - public Range(final Integer min, final Integer max) { + Range(final Integer min, final Integer max) { this.min = min; this.max = max; } - public Integer getMin() { + Integer getMin() { return min; } - public Integer getMax() { + Integer getMax() { return max; } } @@ -1328,10 +1336,12 @@ void listElementConverter() { } @ConfigMapping(prefix = "my-app.rest-config.my-client") - public interface MyRestClientConfig { + interface MyRestClientConfig { @WithParentName Optional client(); + Map map(); + interface RestClientConfig { URI baseUri(); @@ -1357,7 +1367,7 @@ interface Endpoint { @Test void envPropertiesWithoutDottedProperties() { - Map env = new HashMap() { + Map env = new HashMap<>() { { put("MY_APP_REST_CONFIG_MY_CLIENT_BASE_URI", "http://localhost:8080"); put("MY_APP_REST_CONFIG_MY_CLIENT_KEYSTORE_PATH", "config/keystores/my-keys.p12"); @@ -1365,6 +1375,10 @@ void envPropertiesWithoutDottedProperties() { put("MY_APP_REST_CONFIG_MY_CLIENT_ENDPOINTS_0__PATH", "/hello"); put("MY_APP_REST_CONFIG_MY_CLIENT_ENDPOINTS_0__METHODS_0_", "GET"); put("MY_APP_REST_CONFIG_MY_CLIENT_ENDPOINTS_0__METHODS_1_", "POST"); + + put("MY_APP_REST_CONFIG_MY_CLIENT_MAP__MY_KEY__BASE_URI", "http://localhost:9090"); + put("MY_APP_REST_CONFIG_MY_CLIENT_MAP__MY_KEY__KEYSTORE_PATH", "path"); + put("MY_APP_REST_CONFIG_MY_CLIENT_MAP__MY_KEY__KEYSTORE_PASSWORD", "password"); } }; @@ -1395,6 +1409,15 @@ void envPropertiesWithoutDottedProperties() { assertEquals("/hello", mapping.client().get().endpoints().get(0).path()); assertEquals("GET", mapping.client().get().endpoints().get(0).methods().get(0)); assertEquals("POST", mapping.client().get().endpoints().get(0).methods().get(1)); + + assertTrue(properties.contains("my-app.rest-config.my-client.map.\"my.key\".base-uri")); + assertTrue(properties.contains("my-app.rest-config.my-client.map.\"my.key\".keystore.path")); + assertTrue(properties.contains("my-app.rest-config.my-client.map.\"my.key\".keystore.password")); + + RestClientConfig myDotKey = mapping.map().get("my.key"); + assertEquals(URI.create("http://localhost:9090"), myDotKey.baseUri()); + assertEquals(Paths.get("path"), myDotKey.keystore().path()); + assertEquals("password", myDotKey.keystore().password()); } @ConfigMapping(prefix = "optionals") @@ -1490,7 +1513,7 @@ void nestedOptionalsGroupMap() { } @ConfigMapping(prefix = "optional-map") - public interface NestedOptionalMapGroup { + interface NestedOptionalMapGroup { Optional enable(); Map> map(); @@ -1505,9 +1528,11 @@ void nestedOptionalAndMaps() { SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(NestedOptionalMapGroup.class) .withSources(config("optional-map.enable", "true")) - .withSources(config("optional-map.map.filter.default.enable", "false", + .withSources(config( + "optional-map.map.filter.default.enable", "false", "optional-map.map.filter.get-jokes-uni.enable", "true")) - .withSources(config("optional-map.map.client.reaction-api.enable", "true", + .withSources(config( + "optional-map.map.client.reaction-api.enable", "true", "optional-map.map.client.setup-api.enable", "true")) .build(); @@ -1531,4 +1556,1054 @@ void nestedOptionalAndMaps() { assertTrue(mapping.map().get("client").get("setup-api").enable().isPresent()); assertTrue(mapping.map().get("client").get("setup-api").enable().get()); } + + @ConfigMapping(prefix = "optional") + interface OptionalExpressions { + Optional expression(); + + OptionalInt expressionInt(); + } + + @Test + void optionalExpressions() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(OptionalExpressions.class) + .withSources(config("optional.expression", "${expression}")) + .withSources(config("optional.expression-int", "${expression}")) + .build(); + + OptionalExpressions mapping = config.getConfigMapping(OptionalExpressions.class); + + assertFalse(mapping.expression().isPresent()); + assertFalse(mapping.expressionInt().isPresent()); + } + + @Test + void defaultsBuilderAndMapping() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(DefaultsBuilderAndMapping.class) + .withDefaultValue("server.host", "localhost") + .withDefaultValue(SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN, "false") + .build(); + + DefaultsBuilderAndMapping mapping = config.getConfigMapping(DefaultsBuilderAndMapping.class); + + assertEquals("localhost", config.getRawValue("server.host")); + assertEquals(443, mapping.ssl().port()); + assertEquals(2, mapping.ssl().protocols().size()); + } + + @ConfigMapping(prefix = "server") + interface DefaultsBuilderAndMapping { + Ssl ssl(); + + interface Ssl { + @WithDefault("443") + int port(); + + @WithDefault("TLSv1.3,TLSv1.2") + List protocols(); + } + } + + @Test + void ignoreNestedUnknown() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(IgnoreNestedUnknown.class) + .withSources(config("ignore.nested.value", "value", "ignore.nested.ignore", "ignore")) + .withSources(config("ignore.nested.nested.value", "value", "ignore.nested.nested.ignore", "ignore")) + .withSources(config("ignore.nested.optional.value", "value", "ignore.nested.optional.ignore", "ignore")) + .withSources(config("ignore.nested.list[0].value", "value", "ignore.nested.list[0].ignore", "ignore")) + .withSources(config("ignore.nested.map.key.value", "value", "ignore.nested.map.ignored.ignored", "ignore")) + .withSources(config("ignore.nested.key.value", "parent", "ignore.nested.ignored.ignored", "ignore")) + .withSources(config("ignore.nested.ignore.ignore", "ignore")) + .withMappingIgnore("ignore.**") + .build(); + + IgnoreNestedUnknown mapping = config.getConfigMapping(IgnoreNestedUnknown.class); + + assertEquals("value", mapping.value()); + assertEquals("value", mapping.nested().value()); + assertTrue(mapping.optional().isPresent()); + assertEquals("value", mapping.optional().get().value()); + assertEquals("value", mapping.list().get(0).value()); + assertEquals("value", mapping.map().get("key").value()); + assertEquals("parent", mapping.mapParent().get("key").value()); + } + + @ConfigMapping(prefix = "ignore.nested") + interface IgnoreNestedUnknown { + String value(); + + Nested nested(); + + Optional optional(); + + List list(); + + Map map(); + + @WithParentName + Map mapParent(); + + interface Nested { + String value(); + } + } + + @Test + void withNameDotted() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(WithNameDotted.class) + .withSources(config("with.name.dotted.name", "value")) + .withSources(config("with.name.nested.dotted.name", "value")) + .withSources(config( + "with.name.map.key.name", "value", + "with.name.map.key.dotted.name", "value", + "with.name.map.key.dotted.another.name", "another", + "with.name.map.key.dotted.description", "value")) + .withSources(config( + "with.name.nested.map.key.nested-key.name", "value", + "with.name.nested.map.key.nested-key.dotted.name", "value", + "with.name.nested.map.key.nested-key.dotted.another.name", "another", + "with.name.nested.map.key.nested-key.dotted.description", "value")) + .build(); + + WithNameDotted mapping = config.getConfigMapping(WithNameDotted.class); + + assertEquals("value", mapping.dottedName()); + assertEquals("value", mapping.nested().dottedName()); + assertEquals("default", mapping.nested().dotted().name()); + assertEquals(1, mapping.map().size()); + assertEquals("value", mapping.map().get("key").dottedName()); + assertEquals("value", mapping.map().get("key").name()); + assertEquals("another", mapping.map().get("key").dotted().name()); + assertEquals("value", mapping.map().get("key").dotted().description()); + assertEquals(1, mapping.nestedMap().size()); + assertEquals("value", mapping.nestedMap().get("key").get("nested-key").name()); + assertEquals("another", mapping.nestedMap().get("key").get("nested-key").dotted().name()); + assertEquals("value", mapping.nestedMap().get("key").get("nested-key").dotted().description()); + } + + @ConfigMapping(prefix = "with.name") + interface WithNameDotted { + @WithName("dotted.name") + String dottedName(); + + Nested nested(); + + Map map(); + + @WithName("nested.map") + Map> nestedMap(); + + interface Nested { + @WithName("dotted.name") + String dottedName(); + + @WithDefault("default") + String name(); + + Dotted dotted(); + + interface Dotted { + @WithName("another.name") + @WithDefault("default") + String name(); + + @WithDefault("default") + String description(); + } + } + } + + @Test + void optionalWithConverter() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(OptionalWithConverter.class) + .withSources(config("optional.converter.value", "value")) + .withSources(config("optional.converter.optional-value", "value")) + .withSources(config("optional.converter.primitive", "1")) + .withSources(config("optional.converter.wrapper-int", "1")) + .withSources(config("optional.converter.primitive-array", "dummy")) + .build(); + + OptionalWithConverter mapping = config.getConfigMapping(OptionalWithConverter.class); + + assertEquals("value", mapping.value().value); + assertTrue(mapping.optionalValue().isPresent()); + assertEquals("value", mapping.optionalValue().get().value); + assertEquals(0, mapping.primitive()); + assertTrue(mapping.wrapperInt().isPresent()); + assertEquals(0, mapping.wrapperInt().get()); + } + + @ConfigMapping(prefix = "optional.converter") + interface OptionalWithConverter { + @WithConverter(ValueConverter.class) + Value value(); + + @WithConverter(ValueConverter.class) + Optional optionalValue(); + + @WithConverter(IntConverter.class) + int primitive(); + + @WithConverter(IntConverter.class) + Optional wrapperInt(); + + @WithConverter(ByteArrayConverter.class) + byte[] primitiveArray(); + } + + public static class ValueConverter implements Converter { + @Override + public Value convert(final String value) throws IllegalArgumentException, NullPointerException { + return new Value(value); + } + } + + public static class IntConverter implements Converter { + @Override + public Integer convert(final String value) throws IllegalArgumentException, NullPointerException { + return 0; + } + } + + public static class ByteArrayConverter implements Converter { + @Override + public byte[] convert(String value) throws IllegalArgumentException, NullPointerException { + return value.getBytes(StandardCharsets.UTF_8); + } + } + + static class Value { + String value; + + Value(final String value) { + this.value = value; + } + } + + @Test + void expressionDefaults() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(ExpressionDefaults.class) + .withDefaultValue("expression", "1234") + .build(); + + ExpressionDefaults mapping = config.getConfigMapping(ExpressionDefaults.class); + + assertEquals("1234", mapping.expression()); + } + + @ConfigMapping(prefix = "expression.defaults") + interface ExpressionDefaults { + @WithDefault("${expression}") + String expression(); + } + + @Test + void mapKeys() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(MapKeys.class) + .withSources(config( + "keys.map.one", "1", + "keys.map.one.two", "2", + "keys.map.one.two.three", "3", + "keys.map.\"one.two.three.four\"", "4")) + .withSources(config( + "keys.list.one[0]", "1", + "keys.list.one.two[0]", "2", + "keys.list.one.two.three[0]", "3", + "keys.list.\"one.two.three.four\"[0]", "4")) + .build(); + + MapKeys mapping = config.getConfigMapping(MapKeys.class); + + assertEquals(4, mapping.map().size()); + assertEquals("1", mapping.map().get("one")); + assertEquals("2", mapping.map().get("one.two")); + assertEquals("3", mapping.map().get("one.two.three")); + assertEquals("4", mapping.map().get("one.two.three.four")); + + assertEquals("1", mapping.list().get("one").get(0)); + assertEquals("2", mapping.list().get("one.two").get(0)); + assertEquals("3", mapping.list().get("one.two.three").get(0)); + assertEquals("4", mapping.list().get("one.two.three.four").get(0)); + } + + @ConfigMapping(prefix = "keys") + interface MapKeys { + Map map(); + + Map> list(); + } + + @Test + void defaultsPropertyNames() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(DefaultsPropertyNames.class) + .build(); + + DefaultsPropertyNames mapping = config.getConfigMapping(DefaultsPropertyNames.class); + assertEquals("value", mapping.value()); + assertEquals("value", config.getRawValue("defaults.myProperty")); + + Set properties = stream(config.getPropertyNames().spliterator(), false).collect(Collectors.toSet()); + assertTrue(properties.contains("defaults.myProperty")); + } + + @ConfigMapping(prefix = "defaults") + interface DefaultsPropertyNames { + @WithDefault("value") + @WithName("myProperty") + String value(); + } + + @Test + void unnamedMapKeys() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(UnnamedMapKeys.class) + .withSources(config( + "unnamed.map.value", "unnamed", + "unnamed.map.one.value", "one", + "unnamed.map.two.value", "two", + "unnamed.map.\"3.three\".value", "three", + "unnamed.double-map.value", "unnamed", + "unnamed.double-map.one.two.value", "double", + "unnamed.triple-map.value", "unnamed", + "unnamed.triple-map.one.three.value", "unnamed-2-3", + "unnamed.triple-map.one.two.three.value", "triple", + "unnamed.map-list[0].value", "unnamed", + "unnamed.map-list.one[0].value", "one-0", + "unnamed.map-list.one[1].value", "one-1", + "unnamed.map-list.\"3.three\"[0].value", "3.three-0", + "unnamed.parent.value", "unnamed", + "unnamed.parent.one.value", "one")) + .build(); + + UnnamedMapKeys mapping = config.getConfigMapping(UnnamedMapKeys.class); + assertEquals("unnamed", mapping.map().get(null).value()); + assertEquals("one", mapping.map().get("one").value()); + assertEquals("two", mapping.map().get("two").value()); + assertEquals("three", mapping.map().get("3.three").value()); + assertEquals("unnamed", mapping.doubleMap().get(null).get(null).value()); + assertEquals("double", mapping.doubleMap().get("one").get("two").value()); + assertEquals("unnamed", mapping.tripleMap().get("a").get("b").get("c").value()); + assertEquals("triple", mapping.tripleMap().get("one").get("two").get("three").value()); + assertEquals("unnamed", mapping.mapList().get(null).get(0).value()); + assertEquals("one-0", mapping.mapList().get("one").get(0).value()); + assertEquals("one-1", mapping.mapList().get("one").get(1).value()); + assertEquals("3.three-0", mapping.mapList().get("3.three").get(0).value()); + assertEquals("unnamed", mapping.parent().parent().get(null).value()); + assertEquals("one", mapping.parent().parent().get("one").value()); + } + + @ConfigMapping(prefix = "unnamed") + interface UnnamedMapKeys { + @WithUnnamedKey + Map map(); + + Map<@WithUnnamedKey String, Map<@WithUnnamedKey String, Nested>> doubleMap(); + + Map<@WithUnnamedKey("a") String, Map<@WithUnnamedKey("b") String, Map<@WithUnnamedKey("c") String, Nested>>> tripleMap(); + + @WithUnnamedKey + Map> mapList(); + + Parent parent(); + + interface Nested { + String value(); + } + + interface Parent { + @WithParentName + @WithUnnamedKey + Map parent(); + } + } + + @Test + void explicitUnnamedMapKeys() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(UnnamedExplicitMapKeys.class) + .withSources(config( + "unnamed.map.value", "value", + "unnamed.map.unnamed.value", "explicit")) + .build(); + + UnnamedExplicitMapKeys mapping = config.getConfigMapping(UnnamedExplicitMapKeys.class); + assertEquals(1, mapping.map().size()); + assertEquals("explicit", mapping.map().get("unnamed").value()); + } + + @ConfigMapping(prefix = "unnamed") + interface UnnamedExplicitMapKeys { + @WithUnnamedKey("unnamed") + Map map(); + + interface Nested { + String value(); + } + } + + @Test + void ambiguousMapping() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("ambiguous.value", "value")) + .withMapping(AmbiguousMapping.class).build(); + + AmbiguousMapping mapping = config.getConfigMapping(AmbiguousMapping.class); + assertEquals("value", mapping.value()); + assertEquals("value", mapping.nested().get(null).value()); + } + + @ConfigMapping(prefix = "ambiguous") + interface AmbiguousMapping { + String value(); + + @WithParentName + @WithUnnamedKey + Map nested(); + + interface Nested { + String value(); + } + } + + @Test + void withNameMultipleSegments() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(WithNameMultipleSegments.class) + .withSources(config( + "my.property.rest.api.url", "http://localhost:8094", + "my.optional.rest.api.url", "http://localhost:8094", + "my.list[0].rest.api.url", "http://localhost:8094", + "my.map.key.rest.api.url", "http://localhost:8094", + "my.map.\"quoted-key\".rest.api.url", "http://localhost:8094", + "my.map-list.key[0].rest.api.url", "http://localhost:8094")) + .build(); + + WithNameMultipleSegments mapping = config.getConfigMapping(WithNameMultipleSegments.class); + assertEquals("http://localhost:8094", mapping.property().apiUrl()); + assertTrue(mapping.optional().isPresent()); + assertEquals("http://localhost:8094", mapping.optional().get().apiUrl()); + assertEquals("http://localhost:8094", mapping.list().get(0).apiUrl()); + assertEquals("http://localhost:8094", mapping.map().get("key").apiUrl()); + assertEquals("http://localhost:8094", mapping.map().get("quoted-key").apiUrl()); + assertEquals("http://localhost:8094", mapping.mapList().get("key").get(0).apiUrl()); + assertEquals("other", mapping.other()); + assertEquals("other", config.getConfigValue("my.rest.api.other").getValue()); + } + + @ConfigMapping(prefix = "my") + interface WithNameMultipleSegments { + Property property(); + + Optional optional(); + + List list(); + + Map map(); + + Map> mapList(); + + @WithName("rest.api.other") + @WithDefault("other") + String other(); + + interface Property { + @WithName("rest.api.url") + String apiUrl(); + } + } + + @Test + void unmmapedPropertiesLocation() { + ConfigValidationException exception = assertThrows(ConfigValidationException.class, () -> new SmallRyeConfigBuilder() + .withMapping(UnMappedPropertiesLocation.class) + .withSources(config("unmapped.unmapped", "value", "unmapped.another", "value")) + .build()); + + assertEquals("SRCFG00050: unmapped.unmapped in KeyValuesConfigSource does not map to any root", + exception.getProblem(0).getMessage()); + assertEquals("SRCFG00050: unmapped.another in KeyValuesConfigSource does not map to any root", + exception.getProblem(1).getMessage()); + } + + @ConfigMapping(prefix = "unmapped") + interface UnMappedPropertiesLocation { + } + + @Test + void mapDefaults() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(MapDefaults.class) + .withSources(config("map.nested.key.value", "non-default-value")) + .build(); + + MapDefaults mapping = config.getConfigMapping(MapDefaults.class); + Map nested = mapping.nested(); + assertEquals(1, nested.size()); + assertEquals("non-default-value", nested.get("key").value()); + assertEquals("value", nested.get("one").value()); + assertEquals("value", nested.get("two").value()); + assertEquals("another", nested.get("three").another().another()); + assertFalse(nested.get("one").optional().isPresent()); + + Map anotherNested = nested.get("four").anotherNested(); + assertEquals(0, anotherNested.size()); + assertEquals("another", anotherNested.get("one").another()); + assertTrue(anotherNested.get("one").optional().isPresent()); + assertEquals("another", anotherNested.get("one").optional().get()); + + assertEquals(0, mapping.leaf().size()); + assertEquals("value", mapping.leaf().get("one")); + + assertEquals(0, mapping.list().size()); + assertNull(mapping.list().get("one")); + } + + @ConfigMapping(prefix = "map") + interface MapDefaults { + @WithDefaults + Map nested(); + + @WithDefault("value") + Map leaf(); + + @WithDefaults + Map> list(); + + interface Nested { + @WithDefault("value") + String value(); + + AnotherNested another(); + + Optional optional(); + + @WithDefaults + Map anotherNested(); + } + + interface AnotherNested { + @WithDefault("another") + String another(); + + @WithDefault("another") + Optional optional(); + } + } + + @Test + void mapDefaultsWithParentName() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(MapDefaultsWithParentName.class) + .build(); + + MapDefaultsWithParentName mapping = config.getConfigMapping(MapDefaultsWithParentName.class); + + assertEquals(1, mapping.nested().size()); + assertEquals("value", mapping.nested().get("nested").value()); + assertEquals("value", mapping.nested().get("one").value()); + assertEquals("value", mapping.value()); + } + + @ConfigMapping(prefix = "map") + interface MapDefaultsWithParentName { + @WithParentName + @WithDefaults + Map nested(); + + @WithName("another.value") + @WithDefault("value") + String value(); + + interface Nested { + @WithDefault("value") + String value(); + } + } + + @Test + void invalidMapDefaults() { + assertThrows(ConfigValidationException.class, + () -> new SmallRyeConfigBuilder().withMapping(InvalidMapDefaults.class).build()); + } + + @ConfigMapping(prefix = "map") + interface InvalidMapDefaults { + @WithDefaults + Map nested(); + + interface Nested { + String value(); + } + } + + @Test + void emptyDefault() { + assertThrows(ConfigValidationException.class, + () -> new SmallRyeConfigBuilder().withMapping(EmptyDefault.class).build()); + } + + @ConfigMapping(prefix = "empty") + interface EmptyDefault { + @WithDefault("") + String empty(); + } + + @Test + void withConverterListElement() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(WithConverterListElement.class) + .withSources(config( + "converter.list", "one, two", + "converter.elements", "one, two")) + .build(); + + WithConverterListElement mapping = config.getConfigMapping(WithConverterListElement.class); + + assertTrue(mapping.list().isPresent()); + assertEquals("one", mapping.list().get().get(0)); + assertEquals("two", mapping.list().get().get(1)); + assertTrue(mapping.elements().isPresent()); + assertEquals("one", mapping.elements().get().get(0)); + assertEquals("two", mapping.elements().get().get(1)); + } + + @ConfigMapping(prefix = "converter") + interface WithConverterListElement { + Optional<@WithConverter(TrimConverter.class) List> list(); + + Optional> elements(); + } + + public static class TrimConverter implements Converter { + @Override + public String convert(final String value) throws IllegalArgumentException, NullPointerException { + return value.trim(); + } + } + + @Test + void mapKeyQuotes() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(MapKeyQuotes.class) + // TODO - Default Values does not properly sypport quoted keys due to how NameIterator works + .withSources(config("map.values.\"key.quoted\"", "1234", + "map.values.key.\"quoted\"", "1234")) + .build(); + + MapKeyQuotes mapping = config.getConfigMapping(MapKeyQuotes.class); + assertEquals("1234", mapping.values().get("key.quoted")); + assertEquals("1234", mapping.values().get("key.\"quoted\"")); + } + + @ConfigMapping(prefix = "map") + interface MapKeyQuotes { + Map values(); + } + + @Test + void mapWithEnvVarsOnlyInProfile() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("map.env.one", "one", "%dev.map.env.two", "two")) + .withSources(new EnvConfigSource(Map.of("_DEV_MAP_ENV_THREE", "3"), 100)) + .withMapping(MapWithEnvVarsOnlyInProfile.class) + .withProfile("dev") + .build(); + + MapWithEnvVarsOnlyInProfile mapping = config.getConfigMapping(MapWithEnvVarsOnlyInProfile.class); + + assertEquals("one", mapping.map().get("one")); + assertEquals("two", mapping.map().get("two")); + assertEquals("3", mapping.map().get("three")); + } + + @ConfigMapping(prefix = "map.env") + interface MapWithEnvVarsOnlyInProfile { + @WithParentName + Map map(); + } + + @Test + void overrideEnvPropertyName() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(new EnvConfigSource(Map.of( + "ENV_PROPERTY__KEY_ONE__NAME", "one", + "ENV_PROPERTY_KEY_TWO_NAME", "two"), 300)) + .withDefaultValue("env-property.key-two.name", "none") + .withMapping(EnvPropertyName.class) + .build(); + + EnvPropertyName mapping = config.getConfigMapping(EnvPropertyName.class); + assertEquals(2, mapping.nested().size()); + assertEquals("one", mapping.nested().get("key.one").name()); + assertEquals("two", mapping.nested().get("key-two").name()); + } + + @ConfigMapping(prefix = "env-property") + interface EnvPropertyName { + @WithParentName + Map nested(); + + interface Nested { + String name(); + } + } + + @Test + void doNotOverrideBuilderDefault() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withDefaultValue("override.value", "another") + .withDefaultValue("override.map.key.value", "another") + .withMapping(DoNotOverrideBuilderDefault.class) + .build(); + + assertEquals("another", config.getRawValue("override.value")); + assertEquals("another", config.getRawValue("override.map.key.value")); + + DoNotOverrideBuilderDefault mapping = config.getConfigMapping(DoNotOverrideBuilderDefault.class); + assertEquals("another", mapping.value()); + assertEquals("another", mapping.map().get("key").value()); + } + + @ConfigMapping(prefix = "override") + interface DoNotOverrideBuilderDefault { + @WithDefault("value") + String value(); + + @WithDefaults + Map map(); + + interface Nested { + @WithDefault("value") + String value(); + } + } + + @Test + void invalidKeys() { + ConfigValidationException configValidationException = assertThrows(ConfigValidationException.class, + () -> new SmallRyeConfigBuilder() + .withMapping(InvalidKeys.class) + .withSources(config("invalid.value.", "value", "invalid.map.", "value", "invalid.list[0].", "value")) + .build()); + + Set messages = new HashSet<>(); + for (int i = 0; i < configValidationException.getProblemCount(); i++) { + messages.add(configValidationException.getProblem(i).getMessage()); + } + + assertTrue(messages.contains("SRCFG00050: invalid.value. in KeyValuesConfigSource does not map to any root")); + assertTrue(messages.contains("SRCFG00050: invalid.list[0]. in KeyValuesConfigSource does not map to any root")); + assertTrue(messages.contains("SRCFG00050: invalid.map. in KeyValuesConfigSource does not map to any root")); + } + + @ConfigMapping(prefix = "invalid") + interface InvalidKeys { + String value(); + + Map map(); + + List list(); + } + + @Test + void mapWithParentName() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("value", "value", "key.value", "value")) + .withMapping(MapWithParentName.class) + .build(); + + MapWithParentName mapping = config.getConfigMapping(MapWithParentName.class); + + assertEquals("value", mapping.value()); + assertEquals("value", mapping.map().get("key").value()); + } + + @ConfigMapping + interface MapWithParentName { + String value(); + + @WithParentName + Map map(); + + interface Nested { + String value(); + } + } + + @Test + void superOptionals() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "markets.config.default.timezone", "Europe/Berlin", + "markets.config.default.activeFrom", "2099-12-31T08:00", + "markets.config.default.disabledFrom", "2099-12-31T08:00", + "markets.config.de.timezone", "Europe/Berlin", + "markets.config.de.activeFrom", "2099-12-31T08:00", + "markets.config.de.disabledFrom", "2099-12-31T08:00")) + .withMapping(MarketConfigChild.class) + .build(); + + MarketConfigChild mapping = config.getConfigMapping(MarketConfigChild.class); + + assertEquals(2, mapping.config().size()); + + MarketConfig.MarketConfiguration germanMarket = mapping.config().get("de"); + assertTrue(germanMarket.activeFrom().isPresent()); + assertTrue(germanMarket.disabledFrom().isPresent()); + assertTrue(germanMarket.timezone().isPresent()); + assertTrue(germanMarket.partners().isEmpty()); + } + + interface MarketConfig { + Map config(); + + interface MarketConfiguration { + Optional activeFrom(); + + Optional disabledFrom(); + + Optional timezone(); + + Optional> partners(); + } + } + + @ConfigMapping(prefix = "markets", namingStrategy = VERBATIM) + public interface MarketConfigChild extends MarketConfig { + + } + + @Test + void embeddedMapping() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "example.foo", "foo", + "example.config.map.a", "a", + "example.config.map.b", "b", + "example.config.foo", "bar", + "example.config.list", "foo,bar", + "example.config.nested.nested-name", "nested")) + .withMapping(Embedded.class) + .withMapping(Embeddable.class) + .build(); + + Embedded embedded = config.getConfigMapping(Embedded.class); + assertEquals("bar", embedded.foo()); + assertEquals("a", embedded.map().get("a")); + assertEquals("b", embedded.map().get("b")); + assertIterableEquals(List.of("foo", "bar"), embedded.list()); + assertEquals("nested", embedded.nested().nestedName()); + + Embeddable embeddable = config.getConfigMapping(Embeddable.class); + assertEquals("foo", embeddable.foo()); + assertEquals(embedded.map().get("a"), embeddable.config().map().get("a")); + assertEquals(embedded.map().get("b"), embeddable.config().map().get("b")); + assertIterableEquals(embedded.list(), embeddable.config().list()); + assertEquals(embedded.nested().nestedName(), embeddable.config().nested().nestedName()); + } + + @ConfigMapping(prefix = "example.config") + interface Embedded { + String foo(); + + Map map(); + + List list(); + + NestedConfig nested(); + + interface NestedConfig { + String nestedName(); + } + } + + @ConfigMapping(prefix = "example") + interface Embeddable { + String foo(); + + Embedded config(); + } + + @Test + void nestedMaps() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("auth.policy.shared1.roles.root", "admin,user")) + .withMapping(AuthRuntimeConfig.class) + .build(); + + AuthRuntimeConfig mapping = config.getConfigMapping(AuthRuntimeConfig.class); + + assertIterableEquals(List.of("admin", "user"), mapping.rolePolicy().get("shared1").roles().get("root")); + } + + @ConfigMapping(prefix = "auth") + interface AuthRuntimeConfig { + @WithName("policy") + Map rolePolicy(); + + interface PolicyConfig { + Map> roles(); + } + } + + @Test + void ambiguousUnnamedKeys() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "ambiguous.value", "value", + "ambiguous.ambiguous.value", "value")) + .withMapping(AmbiguousUnnamedKeys.class) + .build(); + + AmbiguousUnnamedKeys mapping = config.getConfigMapping(AmbiguousUnnamedKeys.class); + assertEquals(1, mapping.map().size()); + } + + @ConfigMapping(prefix = "ambiguous") + interface AmbiguousUnnamedKeys { + @WithParentName + @WithUnnamedKey("") + Map map(); + + interface Nested { + Optional value(); + + Ambiguous ambiguous(); + + interface Ambiguous { + Optional value(); + } + } + } + + @Test + void ambiguousMapKeyWithGroup() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "ambiguous.query.sql", "sql", + "ambiguous.default.name", "name")) + .withMapping(AmbiguousMapKeyWithGroup.class) + .build(); + + AmbiguousMapKeyWithGroup mapping = config.getConfigMapping(AmbiguousMapKeyWithGroup.class); + assertEquals("sql", mapping.query().sql()); + assertEquals("name", mapping.datasources().get("default").name()); + + config = new SmallRyeConfigBuilder() + .withSources(config( + "ambiguous.query.sql", "sql", + "ambiguous.query.name", "name")) + .withMapping(AmbiguousMapKeyWithGroup.class) + .build(); + + mapping = config.getConfigMapping(AmbiguousMapKeyWithGroup.class); + assertEquals("sql", mapping.query().sql()); + assertEquals("name", mapping.datasources().get("query").name()); + } + + @ConfigMapping(prefix = "ambiguous") + interface AmbiguousMapKeyWithGroup { + Query query(); + + @WithParentName + Map datasources(); + + interface Query { + String sql(); + } + + interface Datasource { + String name(); + } + } + + @Test + void ignorePathsRecursive() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMappingIgnore("ignore.**") + .withMappingIgnore("ignore.nested.nested.ignore") + .withSources(config( + "ignore.ignore", "ignore", + "ignore.nested.something", "ignore", + "ignore.nested.nested.ignore", "ignore")) + .withMapping(IgnorePathsRecursive.class) + .build(); + + assertNotNull(config.getConfigMapping(IgnorePathsRecursive.class)); + + config = new SmallRyeConfigBuilder() + .withMappingIgnore("ignore.nested.nested.ignore") + .withMappingIgnore("ignore.**") + .withSources(config( + "ignore.ignore", "ignore", + "ignore.nested.something", "ignore", + "ignore.nested.nested.ignore", "ignore")) + .withMapping(IgnorePathsRecursive.class) + .build(); + + assertNotNull(config.getConfigMapping(IgnorePathsRecursive.class)); + } + + @ConfigMapping(prefix = "ignore") + interface IgnorePathsRecursive { + } + + @Test + void nestedLeafsMaps() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("maps.one.two", "value")) + .withMapping(NestedLeadfsMaps.class) + .build(); + NestedLeadfsMaps mapping = config.getConfigMapping(NestedLeadfsMaps.class); + assertEquals("value", mapping.doubleMap().get("one").get("two")); + + config = new SmallRyeConfigBuilder() + .withSources(config( + "maps.one.two.three", "value")) + .withMapping(NestedLeadfsMaps.class) + .build(); + mapping = config.getConfigMapping(NestedLeadfsMaps.class); + assertEquals("value", mapping.tripleMap().get("one").get("two").get("three")); + + config = new SmallRyeConfigBuilder() + .withSources(config( + "maps.one.two", "value", + "maps.one.two.three", "value")) + .withMapping(NestedLeadfsMaps.class) + .build(); + mapping = config.getConfigMapping(NestedLeadfsMaps.class); + assertEquals("value", mapping.doubleMap().get("one").get("two")); + assertEquals("value", mapping.tripleMap().get("one").get("two").get("three")); + } + + @ConfigMapping(prefix = "maps") + interface NestedLeadfsMaps { + @WithParentName + Map> doubleMap(); + + @WithParentName + Map>> tripleMap(); + } } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingLoaderTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingLoaderTest.java index 8057161bb..59fb413e6 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingLoaderTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingLoaderTest.java @@ -44,7 +44,7 @@ void loadManually() { @Test void discoverNested() { - ConfigMappingInterface mapping = ConfigMappingLoader.getConfigMappingInterface(ServerNested.class); + ConfigMappingInterface mapping = ConfigMappingLoader.getConfigMapping(ServerNested.class); List nested = mapping.getNested(); assertEquals(4, nested.size()); List> types = nested.stream().map(ConfigMappingInterface::getInterfaceType).collect(toList()); @@ -226,4 +226,32 @@ void parentNested() { assertTrue(mappings.contains(ServerParent.class)); assertTrue(mappings.contains(ServerParent.ServerParentNested.class)); } + + interface MyConfig { + GlobalConfig global(); + + interface CommonConfig { + Optional commonTest(); + + UserConfig user(); + } + + interface GlobalConfig extends CommonConfig { + Optional globalTest(); + } + + interface UserConfig { + Optional name(); + } + } + + @Test + void nestedParents() { + List mappingsMetadata = ConfigMappingLoader.getConfigMappingsMetadata(MyConfig.class); + List> mappings = mappingsMetadata.stream().map(ConfigMappingMetadata::getInterfaceType).collect(toList()); + assertTrue(mappings.contains(MyConfig.class)); + assertTrue(mappings.contains(MyConfig.GlobalConfig.class)); + assertTrue(mappings.contains(MyConfig.CommonConfig.class)); + assertTrue(mappings.contains(MyConfig.UserConfig.class)); + } } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingNamingStrategyTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingNamingStrategyTest.java index fb162c3e9..f85428155 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingNamingStrategyTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingNamingStrategyTest.java @@ -1,5 +1,8 @@ package io.smallrye.config; +import static io.smallrye.config.ConfigMapping.NamingStrategy.KEBAB_CASE; +import static io.smallrye.config.ConfigMapping.NamingStrategy.SNAKE_CASE; +import static io.smallrye.config.ConfigMapping.NamingStrategy.VERBATIM; import static io.smallrye.config.KeyValuesConfigSource.config; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -35,8 +38,8 @@ void namingStrategy() { assertTrue(snake.log().enabled()); } - @ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.VERBATIM) - public interface ServerVerbatimNamingStrategy { + @ConfigMapping(prefix = "server", namingStrategy = VERBATIM) + interface ServerVerbatimNamingStrategy { String theHost(); int thePort(); @@ -48,8 +51,8 @@ interface Log { } } - @ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.SNAKE_CASE) - public interface ServerSnakeNamingStrategy { + @ConfigMapping(prefix = "server", namingStrategy = SNAKE_CASE) + interface ServerSnakeNamingStrategy { String theHost(); int thePort(); @@ -100,8 +103,8 @@ void composedNamingStrategy() { assertEquals("log", kebab.theLog().logAppenders().get(0).logName()); } - @ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.SNAKE_CASE) - public interface ServerComposedSnakeNaming { + @ConfigMapping(prefix = "server", namingStrategy = SNAKE_CASE) + interface ServerComposedSnakeNaming { String theHost(); int thePort(); @@ -109,8 +112,8 @@ public interface ServerComposedSnakeNaming { LogInheritedNaming theLog(); } - @ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.VERBATIM) - public interface ServerComposedVerbatimNaming { + @ConfigMapping(prefix = "server", namingStrategy = VERBATIM) + interface ServerComposedVerbatimNaming { String theHost(); int thePort(); @@ -123,7 +126,7 @@ public interface ServerComposedVerbatimNaming { } @ConfigMapping(prefix = "server") - public interface ServerComposedKebabNaming { + interface ServerComposedKebabNaming { String theHost(); int thePort(); @@ -131,7 +134,7 @@ public interface ServerComposedKebabNaming { LogInheritedNaming theLog(); } - public interface LogInheritedNaming { + interface LogInheritedNaming { boolean isEnabled(); List logAppenders(); @@ -159,12 +162,12 @@ void interfaceAndClass() { } @ConfigProperties(prefix = "server") - public static class ConfigPropertiesNamingVerbatim { + static class ConfigPropertiesNamingVerbatim { String theHost; } @ConfigMapping(prefix = "server") - public interface ConfigMappingNamingKebab { + interface ConfigMappingNamingKebab { String theHost(); @WithParentName @@ -176,8 +179,8 @@ interface Form { } // From https://github.com/quarkusio/quarkus/issues/21407 - @ConfigMapping(prefix = "bugs", namingStrategy = ConfigMapping.NamingStrategy.VERBATIM) - public interface NamingStrategyVerbatimOptionalGroup { + @ConfigMapping(prefix = "bugs", namingStrategy = VERBATIM) + interface NamingStrategyVerbatimOptionalGroup { @WithParentName Map bugs(); @@ -197,7 +200,7 @@ interface Properties { String scriptSelector(); } - @ConfigMapping(namingStrategy = ConfigMapping.NamingStrategy.KEBAB_CASE) + @ConfigMapping(namingStrategy = KEBAB_CASE) interface OverrideNamingStrategyProperties { String feed(); @@ -232,4 +235,52 @@ void namingStrategyVerbatimOptionalGroup() { assertEquals("36936471", mapping.bugs().get("KEY1").override().get().customerId()); assertEquals("RoadRunner_Task1_AAR01", mapping.bugs().get("KEY1").override().get().scriptSelector()); } + + @Test + void namingStrategyDefaults() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(NamingStrategyDefaults.class) + .build(); + + NamingStrategyDefaults mapping = config.getConfigMapping(NamingStrategyDefaults.class); + + assertEquals("value", mapping.verbatimDefault()); + assertEquals("value", mapping.kebabDefaults().kebabDefault()); + assertEquals("value", mapping.snakeDefaults().snakeDefault()); + assertEquals("value", mapping.verbatimDefaults().verbatimDefault()); + + assertEquals("value", config.getRawValue("defaults.verbatimDefault")); + assertEquals("value", config.getRawValue("defaults.kebabDefaults.kebab-default")); + assertEquals("value", config.getRawValue("defaults.snakeDefaults.snake_default")); + assertEquals("value", config.getRawValue("defaults.verbatimDefaults.verbatimDefault")); + } + + @ConfigMapping(prefix = "defaults", namingStrategy = VERBATIM) + interface NamingStrategyDefaults { + @WithDefault("value") + String verbatimDefault(); + + KebabDefaults kebabDefaults(); + + SnakeDefaults snakeDefaults(); + + VerbatimDefaults verbatimDefaults(); + + @ConfigMapping(namingStrategy = KEBAB_CASE) + interface KebabDefaults { + @WithDefault("value") + String kebabDefault(); + } + + @ConfigMapping(namingStrategy = SNAKE_CASE) + interface SnakeDefaults { + @WithDefault("value") + String snakeDefault(); + } + + interface VerbatimDefaults { + @WithDefault("value") + String verbatimDefault(); + } + } } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingReloadableTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingReloadableTest.java new file mode 100644 index 000000000..c859c49a9 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingReloadableTest.java @@ -0,0 +1,58 @@ +package io.smallrye.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.junit.jupiter.api.Test; + +class ConfigMappingReloadableTest { + @Test + void reloadMapping() { + Map properties = new HashMap<>(); + properties.put("reloadable.reload", "value"); + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(ReloadableMapping.class) + .withSources(new PropertiesConfigSource(properties, "", 0)) + .build(); + + ReloadableMapping mapping = config.getConfigMapping(ReloadableMapping.class); + assertEquals("value", mapping.reload()); + + properties.put("reloadable.reload", "reloaded"); + SmallRyeConfig reloadedConfig = new SmallRyeConfigBuilder() + .withMapping(ReloadableMapping.class) + .withSources(new ConfigSource() { + @Override + public Set getPropertyNames() { + Set properties = new HashSet<>(); + config.getPropertyNames().forEach(properties::add); + return properties; + } + + @Override + public String getValue(final String propertyName) { + return config.getRawValue(propertyName); + } + + @Override + public String getName() { + return "Reloadable"; + } + }).build(); + + ReloadableMapping reloadedMapping = reloadedConfig.getConfigMapping(ReloadableMapping.class); + assertEquals("reloaded", reloadedMapping.reload()); + } + + @ConfigMapping(prefix = "reloadable") + interface ReloadableMapping { + String reload(); + } +} diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingWithConverterTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingWithConverterTest.java new file mode 100644 index 000000000..f445ed6f1 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingWithConverterTest.java @@ -0,0 +1,105 @@ +package io.smallrye.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.eclipse.microprofile.config.spi.Converter; +import org.junit.jupiter.api.Test; + +import io.smallrye.config.common.AbstractConverter; + +public class ConfigMappingWithConverterTest { + @ConfigMapping + interface WrongConverterType { + @WithConverter(IntegerConverter.class) + String type(); + + class IntegerConverter implements Converter { + @Override + public Integer convert(final String value) throws IllegalArgumentException, NullPointerException { + return 0; + } + } + } + + @ConfigMapping + interface WrongAbstractConverterType { + @WithConverter(IntegerConverter.class) + String type(); + + class IntegerConverter extends AbstractConverter { + @Override + public Integer convert(final String value) throws IllegalArgumentException, NullPointerException { + return 0; + } + } + } + + @ConfigMapping + interface WrongSuperConverterType { + @WithConverter(IntegerConverter.class) + String type(); + + class IntegerConverter extends SuperConverter { + + } + + class SuperConverter implements Converter { + @Override + public Integer convert(final String value) throws IllegalArgumentException, NullPointerException { + return 0; + } + } + } + + @ConfigMapping + interface WrongPrimitiveConverterType { + @WithConverter(IntegerConverter.class) + double type(); + + class IntegerConverter implements Converter { + + @Override + public Integer convert(final String value) throws IllegalArgumentException, NullPointerException { + return 0; + } + + } + } + + @Test + void wrongConverter() { + assertThrows(IllegalArgumentException.class, () -> config(WrongConverterType.class)); + assertThrows(IllegalArgumentException.class, () -> config(WrongAbstractConverterType.class)); + assertThrows(IllegalArgumentException.class, () -> config(WrongSuperConverterType.class)); + assertThrows(IllegalArgumentException.class, () -> config(WrongPrimitiveConverterType.class)); + } + + @ConfigMapping + interface SuperConverterType { + @WithConverter(SuperConverter.class) + Number number(); + + class SuperConverter implements Converter { + @Override + public Integer convert(final String value) throws IllegalArgumentException, NullPointerException { + return Integer.valueOf(value); + } + } + } + + @Test + void superConverter() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(SuperConverterType.class) + .withDefaultValue("number", "1") + .build(); + + SuperConverterType mapping = config.getConfigMapping(SuperConverterType.class); + assertEquals(1, mapping.number()); + } + + static void config(final Class mapping) { + new SmallRyeConfigBuilder().withMapping(mapping).build(); + } +} diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingsTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingsTest.java index ef1c83c63..0aa2978f3 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigMappingsTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingsTest.java @@ -15,8 +15,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.eclipse.microprofile.config.inject.ConfigProperties; import org.eclipse.microprofile.config.spi.Converter; @@ -93,6 +91,7 @@ void registerProperties() { @Test void validate() { SmallRyeConfig config = new SmallRyeConfigBuilder() + .withConverter(Version.class, 100, new VersionConverter()) .withSources(config("server.host", "localhost", "server.port", "8080", "server.unmapped", "unmapped")) .build(); @@ -112,6 +111,7 @@ void validateWithBuilderOrConfig() { SmallRyeConfig config = new SmallRyeConfigBuilder() .withSources(config("server.host", "localhost", "server.port", "8080", "server.unmapped", "unmapped")) .withMapping(ServerClass.class, "server") + .withConverter(Version.class, 100, new VersionConverter()) .withValidateUnknown(true) .withDefaultValue(SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN, "false") .build(); @@ -127,6 +127,7 @@ void validateDisableOnConfigProperties() { SmallRyeConfig config = new SmallRyeConfigBuilder() .withSources(config("server.host", "localhost", "server.port", "8080", "server.unmapped", "unmapped")) .withMapping(ServerClass.class, "server") + .withConverter(Version.class, 100, new VersionConverter()) .build(); ServerClass server = config.getConfigMapping(ServerClass.class); @@ -190,8 +191,52 @@ void generateToString() { .build(); ToStringMapping mapping = config.getConfigMapping(ToStringMapping.class); - assertEquals("ToStringMapping{host=localhost, port=8080, aliases=[Alias{name=prod-1}, Alias{name=prod-2}]}", - mapping.toString()); + String toString = mapping.toString(); + assertTrue(toString.contains("ToStringMapping{")); + assertTrue(toString.contains("host=localhost")); + assertTrue(toString.contains("port=8080")); + assertTrue(toString.contains("port=8080")); + assertTrue(toString.contains("aliases=[")); + assertTrue(toString.contains("Alias{name=prod-1}")); + assertTrue(toString.contains("Alias{name=prod-2}")); + } + + @ConfigMapping(prefix = "app") + interface SuperToStringMapping { + List foo(); + + String toString(); + + interface Bar extends Foo { + String child(); + + String toString(); + } + + interface Foo { + String parent(); + + String toString(); + } + } + + @Test + void superToString() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(SuperToStringMapping.class) + .withSources(config( + "app.foo[0].parent", "parent", + "app.foo[0].child", "child")) + .build(); + + SuperToStringMapping mapping = config.getConfigMapping(SuperToStringMapping.class); + + assertEquals("parent", mapping.foo().get(0).parent()); + assertEquals("child", mapping.foo().get(0).child()); + + String toString = mapping.toString(); + assertTrue(toString.contains("parent=parent")); + assertTrue(toString.contains("child=child")); } @ConfigMapping(prefix = "server") @@ -273,22 +318,19 @@ interface Nested { @Test void properties() { - Map properties = ConfigMappings.getProperties(configClassWithPrefix(MappedProperties.class)); + ConfigMappings.ConfigClassWithPrefix configClass = configClassWithPrefix(MappedProperties.class); + Map properties = ConfigMappings.getProperties(configClass).get(configClass.getKlass()) + .get(configClass.getPrefix()); assertEquals(3, properties.size()); } @Test void mappedProperties() { - Set mappedProperties = mappedProperties(MappedProperties.class, "mapped.value", "mapped.nested.value", - "mapped.collection[0].value", "mapped.unknown"); + Set mappedProperties = ConfigMappings.mappedProperties(configClassWithPrefix(MappedProperties.class), + Set.of("mapped.value", "mapped.nested.value", "mapped.collection[0].value", "mapped.unknown")); assertEquals(3, mappedProperties.size()); assertTrue(mappedProperties.contains("mapped.value")); assertTrue(mappedProperties.contains("mapped.nested.value")); assertTrue(mappedProperties.contains("mapped.collection[0].value")); } - - private static Set mappedProperties(final Class mappingClass, final String... properties) { - return ConfigMappings.mappedProperties(configClassWithPrefix(mappingClass), - Stream.of(properties).collect(Collectors.toSet())); - } } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigReleaseTest.java b/implementation/src/test/java/io/smallrye/config/ConfigReleaseTest.java new file mode 100644 index 000000000..3e703e5b6 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigReleaseTest.java @@ -0,0 +1,75 @@ +package io.smallrye.config; + +import static io.smallrye.config.KeyValuesConfigSource.config; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.NoSuchElementException; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ConfigReleaseTest { + + private static final Config CONFIG = new SmallRyeConfigBuilder() + .withSources(config("server.host", "localhost", "server.port", "8080")) + .build(); + + private static final ClassLoader THREAD_LOADER = Thread.currentThread().getContextClassLoader(); + private static final ClassLoader PARENT_LOADER = Thread.currentThread().getContextClassLoader().getParent(); + + @BeforeEach + void beforeEach() { + ConfigProviderResolver.instance().releaseConfig(ConfigProvider.getConfig(THREAD_LOADER)); + ConfigProviderResolver.instance().releaseConfig(ConfigProvider.getConfig(PARENT_LOADER)); + ConfigProviderResolver.instance().registerConfig(CONFIG, THREAD_LOADER); + ConfigProviderResolver.instance().registerConfig(CONFIG, PARENT_LOADER); + } + + @Test + void releaseWithoutClassloader() { + Config fromThreadLoader = ConfigProvider.getConfig(THREAD_LOADER); + Config fromPlatformLoader = ConfigProvider.getConfig(PARENT_LOADER); + + assertEquals("localhost", fromThreadLoader.getValue("server.host", String.class)); + assertEquals("localhost", fromPlatformLoader.getValue("server.host", String.class)); + + // this demonstrates the problems highlighted in these issues: + // https://github.com/eclipse/microprofile-config/issues/136#issuecomment-535962313 + // https://github.com/eclipse/microprofile-config/issues/471 + // When we call release with one of the two class loaders, but they will both be released meaning + // the current class loader is removing config from another classloader unexpectedly + ConfigProviderResolver.instance().releaseConfig(fromThreadLoader); + + final Config fromThreadLoader2 = ConfigProvider.getConfig(THREAD_LOADER); + final Config fromPlatformLoader2 = ConfigProvider.getConfig(PARENT_LOADER); + + assertThrows(NoSuchElementException.class, () -> fromThreadLoader2.getValue("server.host", String.class)); + assertThrows(NoSuchElementException.class, () -> fromPlatformLoader2.getValue("server.host", String.class)); + } + + @Test + void releaseWithClassLoader() { + Config fromThreadLoader = ConfigProvider.getConfig(THREAD_LOADER); + Config fromPlatformLoader = ConfigProvider.getConfig(PARENT_LOADER); + + assertEquals("localhost", fromThreadLoader.getValue("server.host", String.class)); + assertEquals("localhost", fromPlatformLoader.getValue("server.host", String.class)); + + // this demonstrates a solution to the problems highlighted in these issues: + // https://github.com/eclipse/microprofile-config/issues/136#issuecomment-535962313 + // https://github.com/eclipse/microprofile-config/issues/471 + // We can explicitly release the config by class loader, leaving other class loaders untouched + ((SmallRyeConfigProviderResolver) ConfigProviderResolver.instance()).releaseConfig( + THREAD_LOADER); + + final Config fromThreadLoader2 = ConfigProvider.getConfig(THREAD_LOADER); + final Config fromPlatformLoader2 = ConfigProvider.getConfig(PARENT_LOADER); + + assertThrows(NoSuchElementException.class, () -> fromThreadLoader2.getValue("server.host", String.class)); + assertEquals("localhost", fromPlatformLoader2.getValue("server.host", String.class)); + } +} diff --git a/implementation/src/test/java/io/smallrye/config/ConfigSourceFactoryTest.java b/implementation/src/test/java/io/smallrye/config/ConfigSourceFactoryTest.java new file mode 100644 index 000000000..8eb0b96df --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigSourceFactoryTest.java @@ -0,0 +1,79 @@ +package io.smallrye.config; + +import static io.smallrye.config.KeyValuesConfigSource.config; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.junit.jupiter.api.Test; + +import io.smallrye.config.ConfigSourceFactory.ConfigurableConfigSourceFactory; + +public class ConfigSourceFactoryTest { + @Test + void mapping() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withSources(new CountConfigurableConfigSourceFactory()) + .withDefaultValue("count.size", "10") + .build(); + + for (int i = 0; i < 10; i++) { + assertEquals(i, config.getValue(i + "", int.class)); + } + } + + @ConfigMapping(prefix = "count") + interface Count { + int size(); + } + + static class CountConfigurableConfigSourceFactory implements ConfigurableConfigSourceFactory { + @Override + public Iterable getConfigSources(final ConfigSourceContext context, final Count config) { + Map properties = new HashMap<>(); + for (int i = 0; i < config.size(); i++) { + properties.put(i + "", i + ""); + } + return Collections.singleton(new PropertiesConfigSource(properties, "", 100)); + } + } + + @Test + void expression() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withSources(new ExpressionConfigSourceFactory()) + .withSources(new EnvConfigSource(Map.of("DEFAULT", "1234"), 100)) + .withMapping(Expression.class) + .build(); + + assertEquals("1234", config.getConfigMapping(Expression.class).value()); + assertEquals("1234", config.getRawValue("factory.expression")); + } + + @ConfigMapping(prefix = "expression") + interface Expression { + @WithDefault("${DEFAULT:}") + String value(); + } + + static class ExpressionConfigSourceFactory implements ConfigSourceFactory { + @Override + public Iterable getConfigSources(final ConfigSourceContext context) { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(new ConfigSourceContext.ConfigSourceContextConfigSource(context)) + .withMapping(Expression.class) + .build(); + + Expression mapping = config.getConfigMapping(Expression.class); + assertEquals("1234", mapping.value()); + + return List.of(new PropertiesConfigSource(Map.of("factory.expression", mapping.value()), "", 100)); + } + } +} diff --git a/implementation/src/test/java/io/smallrye/config/ConfigSourceInterceptorTest.java b/implementation/src/test/java/io/smallrye/config/ConfigSourceInterceptorTest.java index 0e68d3af1..88e79f201 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigSourceInterceptorTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigSourceInterceptorTest.java @@ -11,7 +11,7 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import javax.annotation.Priority; +import jakarta.annotation.Priority; import org.jboss.logging.Logger; import org.junit.jupiter.api.Assertions; diff --git a/implementation/src/test/java/io/smallrye/config/ConfigSourceMapTest.java b/implementation/src/test/java/io/smallrye/config/ConfigSourceMapTest.java deleted file mode 100644 index 8ca411830..000000000 --- a/implementation/src/test/java/io/smallrye/config/ConfigSourceMapTest.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2018 Red Hat, Inc., and individual contributors - * as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.smallrye.config; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.junit.jupiter.api.Test; - -@SuppressWarnings("MismatchedQueryAndUpdateOfCollection") -class ConfigSourceMapTest { - private static final Map MANY_MAP = new HashMap() { - { - put("key-one", "12345"); - put("null-valued-key", null); - put("test", "bar"); - put("fruit", "banana"); - } - }; - private static final ConfigSource MANY_CONF_SRC = new PropertiesConfigSource(MANY_MAP, "test", 100); - private static final Map ONE_MAP = Collections.singletonMap("test", "foo"); - private static final ConfigSource ONE_CONF_SRC = new PropertiesConfigSource(ONE_MAP, "test", 100); - - @Test - void clear() { - try { - new ConfigSourceMap(ONE_CONF_SRC).clear(); - fail("Expected exception"); - } catch (UnsupportedOperationException expected) { - } - } - - @Test - void compute() { - try { - new ConfigSourceMap(ONE_CONF_SRC).compute("piano", (k, v) -> "player"); - fail("Expected exception"); - } catch (UnsupportedOperationException expected) { - } - } - - @Test - void computeIfAbsent() { - try { - //noinspection ExcessiveLambdaUsage - new ConfigSourceMap(ONE_CONF_SRC).computeIfAbsent("piano", k -> "player"); - fail("Expected exception"); - } catch (UnsupportedOperationException expected) { - } - } - - @Test - void computeIfPresent() { - try { - new ConfigSourceMap(ONE_CONF_SRC).computeIfPresent("test", (k, v) -> "bar"); - fail("Expected exception"); - } catch (UnsupportedOperationException expected) { - } - } - - @Test - void containsKey() { - final ConfigSourceMap csm = new ConfigSourceMap(MANY_CONF_SRC); - assertTrue(csm.containsKey("test")); - assertTrue(csm.containsKey("null-valued-key")); - assertFalse(csm.containsKey("missing-key")); - } - - @Test - void containsValue() { - final ConfigSourceMap csm = new ConfigSourceMap(MANY_CONF_SRC); - assertTrue(csm.containsValue("12345")); - assertFalse(csm.containsValue("apple")); - assertTrue(csm.containsValue(null)); - } - - @Test - void equals() { - assertEquals(MANY_MAP, new ConfigSourceMap(MANY_CONF_SRC)); - } - - @Test - void forEach() { - final Set need = new HashSet<>(MANY_MAP.keySet()); - final ConfigSourceMap csm = new ConfigSourceMap(MANY_CONF_SRC); - csm.forEach((k, v) -> { - assertEquals(MANY_MAP.get(k), v); - assertTrue(need.remove(k)); - }); - assertTrue(need.isEmpty(), "keys left in set"); - } - - @Test - void get() { - final ConfigSourceMap csm = new ConfigSourceMap(MANY_CONF_SRC); - assertEquals("bar", csm.get("test")); - assertNull(csm.get("null-valued-key")); - assertNull(csm.get("nope")); - } - - @Test - void getOrDefault() { - final ConfigSourceMap csm = new ConfigSourceMap(MANY_CONF_SRC); - assertEquals("bar", csm.getOrDefault("test", "foo")); - assertNull(csm.getOrDefault("null-valued-key", "oops")); - assertEquals("yes", csm.getOrDefault("nope", "yes")); - } - - @Test - void hashCodeSource() { - assertEquals(MANY_MAP.hashCode(), new ConfigSourceMap(MANY_CONF_SRC).hashCode()); - } - - @Test - void isEmpty() { - assertTrue(new ConfigSourceMap(new PropertiesConfigSource(Collections.emptyMap(), "test", 100)).isEmpty()); - assertFalse(new ConfigSourceMap(ONE_CONF_SRC).isEmpty()); - assertFalse(new ConfigSourceMap(MANY_CONF_SRC).isEmpty()); - } - - @Test - void keySet() { - assertEquals(ONE_CONF_SRC.getPropertyNames(), new ConfigSourceMap(ONE_CONF_SRC).keySet()); - assertEquals(MANY_CONF_SRC.getPropertyNames(), new ConfigSourceMap(MANY_CONF_SRC).keySet()); - } - - @Test - void merge() { - try { - new ConfigSourceMap(MANY_CONF_SRC).merge("test", "bar", (k, v) -> "oops"); - fail("Expected exception"); - } catch (UnsupportedOperationException expected) { - } - } - - @Test - void put() { - try { - new ConfigSourceMap(ONE_CONF_SRC).put("bees", "bzzzz"); - fail("Expected exception"); - } catch (UnsupportedOperationException expected) { - } - } - - @Test - void putAll() { - try { - new ConfigSourceMap(ONE_CONF_SRC).putAll(Collections.singletonMap("bees", "bzzzz")); - fail("Expected exception"); - } catch (UnsupportedOperationException expected) { - } - } - - @Test - void putIfAbsent() { - try { - new ConfigSourceMap(ONE_CONF_SRC).putIfAbsent("bees", "bzzzz"); - fail("Expected exception"); - } catch (UnsupportedOperationException expected) { - } - new ConfigSourceMap(ONE_CONF_SRC).putIfAbsent("test", "not absent"); - } - - @Test - void remove1() { - try { - new ConfigSourceMap(MANY_CONF_SRC).remove("test"); - fail("Expected exception"); - } catch (UnsupportedOperationException expected) { - } - } - - @Test - void remove2() { - try { - new ConfigSourceMap(MANY_CONF_SRC).remove("test"); - fail("Expected exception"); - } catch (UnsupportedOperationException expected) { - } - } - - @Test - void replace2() { - try { - new ConfigSourceMap(MANY_CONF_SRC).replace("test", "oops"); - fail("Expected exception"); - } catch (UnsupportedOperationException expected) { - } - } - - @Test - void replace3() { - try { - new ConfigSourceMap(MANY_CONF_SRC).replace("test", "bar", "oops"); - fail("Expected exception"); - } catch (UnsupportedOperationException expected) { - } - try { - // false or exception are OK - assertFalse(new ConfigSourceMap(MANY_CONF_SRC).replace("test", "nope", "oops")); - } catch (UnsupportedOperationException expected) { - } - } - - @Test - void replaceAll() { - try { - new ConfigSourceMap(MANY_CONF_SRC).replaceAll((k, v) -> "oops"); - fail("Expected exception"); - } catch (UnsupportedOperationException expected) { - } - } - - @Test - void size() { - assertEquals(MANY_MAP.size(), new ConfigSourceMap(MANY_CONF_SRC).size()); - assertEquals(ONE_MAP.size(), new ConfigSourceMap(ONE_CONF_SRC).size()); - } -} diff --git a/implementation/src/test/java/io/smallrye/config/ConfigSourcePropertySubstitutionTest.java b/implementation/src/test/java/io/smallrye/config/ConfigSourcePropertySubstitutionTest.java index bff25713c..fc0d66f2a 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigSourcePropertySubstitutionTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigSourcePropertySubstitutionTest.java @@ -7,7 +7,7 @@ class ConfigSourcePropertySubstitutionTest { @Test void interceptor() { - SmallRyeConfig config = (SmallRyeConfig) buildConfig("my.prop", "${prop.replace}", "prop.replace", "1234"); + SmallRyeConfig config = buildConfig("my.prop", "${prop.replace}", "prop.replace", "1234").unwrap(SmallRyeConfig.class); final String value = config.getValue("my.prop", String.class); Assertions.assertEquals("1234", value); diff --git a/implementation/src/test/java/io/smallrye/config/ConfigValueConfigSourceWrapperTest.java b/implementation/src/test/java/io/smallrye/config/ConfigValueConfigSourceWrapperTest.java index 92257975f..6e6c843a4 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigValueConfigSourceWrapperTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigValueConfigSourceWrapperTest.java @@ -54,6 +54,6 @@ void getOrdinal() { } private static ConfigValueConfigSource config() { - return ConfigValueConfigSourceWrapper.wrap(KeyValuesConfigSource.config("my.prop", "1234")); + return SmallRyeConfigSources.ConfigValueConfigSourceWrapper.wrap(KeyValuesConfigSource.config("my.prop", "1234")); } } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigValueConversionRulesExceptionsTest.java b/implementation/src/test/java/io/smallrye/config/ConfigValueConversionRulesExceptionsTest.java index d9fec18f2..3b26e7729 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigValueConversionRulesExceptionsTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigValueConversionRulesExceptionsTest.java @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test; /** - * + * * Test sensible error messages are output when edge case config value conversion Exceptions occur * */ diff --git a/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesConfigSourceTest.java b/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesConfigSourceTest.java index 215f5d8e2..6f15e6331 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesConfigSourceTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesConfigSourceTest.java @@ -13,7 +13,7 @@ class ConfigValuePropertiesConfigSourceTest { @Test void interceptor() throws Exception { - SmallRyeConfig config = (SmallRyeConfig) buildConfig(); + SmallRyeConfig config = buildConfig().unwrap(SmallRyeConfig.class); assertEquals("1", config.getValue("my.prop", String.class)); assertEquals("20", config.getValue("my.prop.20", String.class)); diff --git a/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesTest.java b/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesTest.java index 896540b1b..3b273bc7d 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigValuePropertiesTest.java @@ -9,7 +9,8 @@ class ConfigValuePropertiesTest { @Test void singleLine() throws Exception { - final ConfigValueProperties map = new ConfigValueProperties("config", 1); + final ConfigValuePropertiesConfigSource.ConfigValueProperties map = new ConfigValuePropertiesConfigSource.ConfigValueProperties( + "config", 1); map.load(new StringReader("key=value")); assertEquals(1, map.get("key").getLineNumber()); @@ -17,7 +18,8 @@ void singleLine() throws Exception { @Test void multipleLines() throws Exception { - final ConfigValueProperties map = new ConfigValueProperties("config", 1); + final ConfigValuePropertiesConfigSource.ConfigValueProperties map = new ConfigValuePropertiesConfigSource.ConfigValueProperties( + "config", 1); map.load(new StringReader( "key=value\n" + "key2=value\n" + @@ -48,7 +50,8 @@ void multipleLines() throws Exception { @Test void comments() throws Exception { - final ConfigValueProperties map = new ConfigValueProperties("config", 1); + final ConfigValuePropertiesConfigSource.ConfigValueProperties map = new ConfigValuePropertiesConfigSource.ConfigValueProperties( + "config", 1); map.load(new StringReader( "key=value\n" + "key2=value\n" + @@ -64,7 +67,8 @@ void comments() throws Exception { @Test void wrapValue() throws Exception { - final ConfigValueProperties map = new ConfigValueProperties("config", 1); + final ConfigValuePropertiesConfigSource.ConfigValueProperties map = new ConfigValuePropertiesConfigSource.ConfigValueProperties( + "config", 1); map.load(new StringReader( "key=value\\wrap\n" + "key2=value\\\nwrap\n" + diff --git a/implementation/src/test/java/io/smallrye/config/ConfigValueTest.java b/implementation/src/test/java/io/smallrye/config/ConfigValueTest.java index eb4f54559..16de293cb 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigValueTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigValueTest.java @@ -44,6 +44,19 @@ void configValueEquals() { assertEquals(o2, o1); } + @Test + void configValueCloning() { + ConfigValue o1 = io.smallrye.config.ConfigValue.builder().withName("my.prop").build(); + ConfigValue o2 = ((io.smallrye.config.ConfigValue) o1).from().build(); + assertEquals(o2, o1); + + o1 = io.smallrye.config.ConfigValue.builder().withName("my.prop").withValue("value").build(); + o2 = ((io.smallrye.config.ConfigValue) o1).from().withName("my.prop.cloned").build(); + assertNotEquals(o2, o1); + assertEquals("my.prop.cloned", o2.getName()); + assertEquals(o1.getValue(), o2.getValue()); + } + public static class ConfigValueConfigSource implements ConfigSource { private final Map properties; diff --git a/implementation/src/test/java/io/smallrye/config/ConvertersTest.java b/implementation/src/test/java/io/smallrye/config/ConvertersTest.java index 9a7789b96..f16928d2a 100644 --- a/implementation/src/test/java/io/smallrye/config/ConvertersTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConvertersTest.java @@ -17,6 +17,7 @@ import static org.junit.jupiter.api.Assertions.*; +import java.nio.file.Path; import java.time.LocalDate; import java.time.chrono.ChronoLocalDate; import java.util.ArrayList; @@ -360,6 +361,17 @@ void pattern() { "Unexpected value for pattern"); } + @Test + void path() { + final SmallRyeConfig config = buildConfig("simple.path", "/test", "path.leading.space", " test"); + assertEquals(Path.of("/test"), + config.getValue("simple.path", Path.class), + "Unexpected value for path"); + assertEquals(Path.of(" test"), + config.getValue("path.leading.space", Path.class), + "Unexpected value for path"); + } + @Test void nulls() { final Config config = ConfigProvider.getConfig(); diff --git a/implementation/src/test/java/io/smallrye/config/CustomConverterTest.java b/implementation/src/test/java/io/smallrye/config/CustomConverterTest.java index d19774f26..bf81ce8cb 100644 --- a/implementation/src/test/java/io/smallrye/config/CustomConverterTest.java +++ b/implementation/src/test/java/io/smallrye/config/CustomConverterTest.java @@ -11,9 +11,11 @@ import java.net.InetAddress; import java.util.ArrayList; +import java.util.Collection; import java.util.Locale; import java.util.NoSuchElementException; import java.util.UUID; +import java.util.function.IntFunction; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.Converter; @@ -40,12 +42,8 @@ void characterConverter() { @Test void explicitConverter() { // setup - SmallRyeConfig config = (SmallRyeConfig) buildConfig("my.prop", "1234");// sanity check - final Converter customConverter = new Converter() { - public Integer convert(final String value) { - return Integer.valueOf(Integer.parseInt(value) * 2); - } - }; + SmallRyeConfig config = buildConfig("my.prop", "1234").unwrap(SmallRyeConfig.class);// sanity check + Converter customConverter = value -> Integer.parseInt(value) * 2; // compare against the implicit converter // regular read assertEquals(1234, config.getValue("my.prop", Integer.class).intValue()); @@ -56,8 +54,9 @@ public Integer convert(final String value) { assertEquals(2468, config.getOptionalValue("my.prop", customConverter).orElseThrow(IllegalStateException::new).intValue()); // collection - assertEquals(singletonList(Integer.valueOf(1234)), config.getValues("my.prop", Integer.class, ArrayList::new)); - assertEquals(singletonList(Integer.valueOf(2468)), config.getValues("my.prop", customConverter, ArrayList::new)); + assertEquals(singletonList(1234), config.getValues("my.prop", Integer.class, ArrayList::new)); + assertEquals(singletonList(2468), + config.getValues("my.prop", customConverter, (IntFunction>) value -> new ArrayList<>())); // check missing behavior // regular read try { @@ -80,7 +79,7 @@ public Integer convert(final String value) { } catch (NoSuchElementException expected) { } try { - config.getValues("missing.prop", customConverter, ArrayList::new); + config.getValues("missing.prop", customConverter, (IntFunction>) value -> new ArrayList<>()); fail("Expected exception"); } catch (NoSuchElementException expected) { } @@ -109,7 +108,7 @@ void UUID() { assertEquals(uuidUUIDTruth, config.getValue("uuid.shouting", UUID.class), "Uppercase UUID incorrectly converted"); // Check UUIDs work fine in arrays - ArrayList values = ((SmallRyeConfig) config).getValues("uuid.multiple", UUID.class, ArrayList::new); + ArrayList values = config.unwrap(SmallRyeConfig.class).getValues("uuid.multiple", UUID.class, ArrayList::new); assertEquals(uuidUUIDTruth, values.get(0), "Unexpected list item in UUID config"); assertEquals(secondUuidUUIDTruth, values.get(1), "Unexpected list item in UUID config"); diff --git a/implementation/src/test/java/io/smallrye/config/DefaultValuesTest.java b/implementation/src/test/java/io/smallrye/config/DefaultValuesTest.java index 9107e9ae5..921f395be 100644 --- a/implementation/src/test/java/io/smallrye/config/DefaultValuesTest.java +++ b/implementation/src/test/java/io/smallrye/config/DefaultValuesTest.java @@ -1,8 +1,10 @@ package io.smallrye.config; +import static io.smallrye.config.KeyValuesConfigSource.config; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; @@ -10,7 +12,7 @@ class DefaultValuesTest { @Test void defaultValue() { SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(KeyValuesConfigSource.config("my.prop", "1234")) + .withSources(config("my.prop", "1234")) .withDefaultValue("my.prop", "1234") .withDefaultValue("my.prop.default", "1234") .build(); @@ -22,16 +24,45 @@ void defaultValue() { @Test void defaultValuesMap() { SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(KeyValuesConfigSource.config("my.prop", "1234")) - .withDefaultValues(new HashMap() { - { - put("my.prop", "1234"); - put("my.prop.default", "1234"); - } - }) + .withSources(config("my.value", "5678")) + .withDefaultValues(Map.of( + "my.value", "1234", + "my.default-value", "1234", + "my.list", "1234", + "my.map.key", "1234", + "my.list-nested[0].value", "1234", + "my.map-nested.key.value", "1234")) + .withMapping(DefaultValues.class) .build(); - assertEquals("1234", config.getRawValue("my.prop")); - assertEquals("1234", config.getRawValue("my.prop.default")); + DefaultValues mapping = config.getConfigMapping(DefaultValues.class); + + assertEquals("5678", config.getRawValue("my.value")); + assertEquals("1234", config.getRawValue("my.default-value")); + assertEquals("5678", mapping.value()); + assertEquals("1234", mapping.defaultValue()); + assertEquals("1234", mapping.list().get(0)); + assertEquals("1234", mapping.map().get("key")); + assertEquals("1234", mapping.listNested().get(0).value()); + assertEquals("1234", mapping.mapNested().get("key").value()); + } + + @ConfigMapping(prefix = "my") + interface DefaultValues { + String value(); + + String defaultValue(); + + List list(); + + Map map(); + + List listNested(); + + Map mapNested(); + + interface Nested { + String value(); + } } } diff --git a/implementation/src/test/java/io/smallrye/config/DotEnvConfigSourceProviderTest.java b/implementation/src/test/java/io/smallrye/config/DotEnvConfigSourceProviderTest.java index 7a52dae09..e86326d03 100644 --- a/implementation/src/test/java/io/smallrye/config/DotEnvConfigSourceProviderTest.java +++ b/implementation/src/test/java/io/smallrye/config/DotEnvConfigSourceProviderTest.java @@ -3,6 +3,8 @@ import static io.smallrye.config.DotEnvConfigSourceProvider.dotEnvSources; import static io.smallrye.config.SecuritySupport.getContextClassLoader; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.FileOutputStream; import java.nio.file.Path; @@ -22,8 +24,6 @@ void dotEnvSource(@TempDir Path tempDir) throws Exception { } SmallRyeConfig config = new SmallRyeConfigBuilder() - .addDefaultSources() - .addDiscoveredSources() .addDefaultInterceptors() .withSources(dotEnvSources(tempDir.resolve(".env").toFile().toURI().toString(), getContextClassLoader())) .build(); @@ -63,8 +63,6 @@ void dotEnvSourceProfiles(@TempDir Path tempDir) throws Exception { } SmallRyeConfig config = new SmallRyeConfigBuilder() - .addDefaultSources() - .addDiscoveredSources() .addDefaultInterceptors() .withProfile("common,dev") .withSources(dotEnvSources(tempDir.resolve(".env").toFile().toURI().toString(), getContextClassLoader())) @@ -77,4 +75,37 @@ void dotEnvSourceProfiles(@TempDir Path tempDir) throws Exception { assertEquals("dev", config.getRawValue("my.prop.profile")); assertEquals("dev", config.getRawValue("MY_PROP_PROFILE")); } + + @Test + void dotEnvSourceConvertNames(@TempDir Path tempDir) throws Exception { + Properties envProperties = new Properties(); + envProperties.setProperty("MY-PROP", "1234"); + envProperties.setProperty("FOO_BAR_BAZ", "1234"); + try (FileOutputStream out = new FileOutputStream(tempDir.resolve(".env").toFile())) { + envProperties.store(out, null); + } + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withSources(dotEnvSources(tempDir.resolve(".env").toFile().toURI().toString(), getContextClassLoader())) + .build(); + + assertEquals("1234", config.getRawValue("my.prop")); + assertEquals("1234", config.getRawValue("foo.bar.baz")); + } + + @Test + void missing() { + new SmallRyeConfigBuilder().withSources(new DotEnvConfigSourceProvider()).build(); + assertTrue(true); + + SmallRyeConfigBuilder failBuilder = new SmallRyeConfigBuilder().withSources(new DotEnvConfigSourceProvider() { + @Override + protected boolean failOnMissingFile() { + return true; + } + }); + + assertThrows(IllegalArgumentException.class, () -> failBuilder.build()); + } } diff --git a/implementation/src/test/java/io/smallrye/config/EnvConfigSourceTest.java b/implementation/src/test/java/io/smallrye/config/EnvConfigSourceTest.java index 76eb50aa1..d8e6e6032 100644 --- a/implementation/src/test/java/io/smallrye/config/EnvConfigSourceTest.java +++ b/implementation/src/test/java/io/smallrye/config/EnvConfigSourceTest.java @@ -15,7 +15,12 @@ */ package io.smallrye.config; +import static io.smallrye.config.Converters.BOOLEAN_CONVERTER; +import static io.smallrye.config.Converters.STRING_CONVERTER; +import static io.smallrye.config.KeyValuesConfigSource.config; +import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; import static java.util.stream.StreamSupport.stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -25,15 +30,17 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; import org.eclipse.microprofile.config.spi.ConfigSource; import org.junit.jupiter.api.Test; +import io.smallrye.config.EnvConfigSource.EnvName; + /** * @author Jeff Mesnil (c) 2018 Red Hat inc. */ @@ -52,7 +59,7 @@ void conversionOfEnvVariableNames() { assertFalse(cs.getPropertyNames().contains("smallrye_mp_config_prop")); assertEquals(envProp, cs.getValue("smallrye.mp.config.prop")); - assertFalse(cs.getPropertyNames().contains("smallrye.mp.config.prop")); + assertTrue(cs.getPropertyNames().contains("smallrye.mp.config.prop")); assertEquals(envProp, cs.getValue("SMALLRYE.MP.CONFIG.PROP")); assertFalse(cs.getPropertyNames().contains("SMALLRYE.MP.CONFIG.PROP")); @@ -65,6 +72,8 @@ void conversionOfEnvVariableNames() { assertEquals("1234", cs.getValue("smallrye_mp_config_prop_lower")); assertTrue(cs.getPropertyNames().contains("smallrye_mp_config_prop_lower")); + + assertEquals("1234", cs.getValue("smallrye/mp/config/prop")); } @Test @@ -84,12 +93,10 @@ void empty() { assertTrue( stream(config.getPropertyNames().spliterator(), false).collect(toList()).contains("SMALLRYE_MP_CONFIG_EMPTY")); - ConfigSource envConfigSource = StreamSupport.stream(config.getConfigSources().spliterator(), false) - .filter(configSource -> configSource.getName().equals("EnvConfigSource")) - .findFirst() - .get(); + Optional envConfigSource = config.getConfigSource("EnvConfigSource"); - assertEquals("", envConfigSource.getValue("SMALLRYE_MP_CONFIG_EMPTY")); + assertTrue(envConfigSource.isPresent()); + assertEquals("", envConfigSource.get().getValue("SMALLRYE_MP_CONFIG_EMPTY")); } @Test @@ -98,12 +105,12 @@ void ordinal() { ConfigSource configSource = config.getConfigSources().iterator().next(); assertTrue(configSource instanceof EnvConfigSource); - assertEquals(configSource.getOrdinal(), 301); + assertEquals(301, configSource.getOrdinal()); } @Test void indexed() { - Map env = new HashMap() { + Map env = new HashMap<>() { { put("INDEXED_0_", "foo"); put("INDEXED_0__PROP", "bar"); @@ -125,24 +132,372 @@ void indexed() { @Test void numbers() { - Map env = new HashMap() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withSources(new EnvConfigSource(Map.of("999_MY_VALUE", "foo", "_999_MY_VALUE", "bar"), 300)) + .build(); + + assertEquals("foo", config.getRawValue("999.my.value")); + assertEquals("foo", config.getRawValue("999_MY_VALUE")); + assertEquals("bar", config.getRawValue("_999_MY_VALUE")); + assertEquals("bar", config.getRawValue("%999.my.value")); + } + + @Test + void map() { + Map env = new HashMap<>() { { - put("999_MY_VALUE", "foo"); - put("_999_MY_VALUE", "bar"); + put("TEST_LANGUAGE__DE_ETR__", "Einfache Sprache"); + put("TEST_LANGUAGE_PT_BR", "FROM ENV"); } }; EnvConfigSource envConfigSource = new EnvConfigSource(env, 300); SmallRyeConfig config = new SmallRyeConfigBuilder() .addDefaultInterceptors() + .withSources(config("test.language.pt-br", "value")) .withSources(envConfigSource) .build(); - Set properties = stream(config.getPropertyNames().spliterator(), false).collect(Collectors.toSet()); - assertEquals(4, properties.size()); - assertTrue(properties.contains("999_MY_VALUE")); - assertTrue(properties.contains("999.my.value")); - assertTrue(properties.contains("_999_MY_VALUE")); - assertTrue(properties.contains("%999.my.value")); + assertEquals("Einfache Sprache", config.getRawValue("test.language.\"de.etr\"")); + + Map map = config.getValues("test.language", STRING_CONVERTER, STRING_CONVERTER); + assertEquals(map.get("de.etr"), "Einfache Sprache"); + assertEquals(map.get("pt-br"), "FROM ENV"); + } + + @Test + void envEquals() { + assertTrue(EnvName.equals("", new String(""))); + assertTrue(EnvName.equals(" ", new String(" "))); + assertFalse(EnvName.equals(" ", new String("foo.bar"))); + assertFalse(EnvName.equals(" ", new String("FOO_BAR"))); + assertFalse(EnvName.equals("foo.bar", new String(""))); + assertFalse(EnvName.equals("FOO_BAR", new String(""))); + + assertFalse(EnvName.equals("BAR", new String("foo.bar"))); + assertFalse(EnvName.equals("foo.bar", new String("BAR"))); + + assertTrue(envSourceEquals("FOO_BAR", new String("FOO_BAR"))); + assertTrue(envSourceEquals("FOO_BAR", new String("foo.bar"))); + assertTrue(envSourceEquals("FOO_BAR", new String("FOO.BAR"))); + assertTrue(envSourceEquals("FOO_BAR", new String("foo-bar"))); + assertTrue(envSourceEquals("FOO_BAR", new String("foo_bar"))); + + assertTrue(EnvName.equals("foo.bar", new String("foo.bar"))); + assertTrue(EnvName.equals("foo-bar", new String("foo-bar"))); + assertTrue(EnvName.equals("foo.bar", new String("FOO_BAR"))); + assertTrue(EnvName.equals("FOO.BAR", new String("FOO_BAR"))); + assertTrue(EnvName.equals("foo-bar", new String("FOO_BAR"))); + assertTrue(EnvName.equals("foo_bar", new String("FOO_BAR"))); + + assertTrue(EnvName.equals("FOO__BAR__BAZ", new String("foo.\"bar\".baz"))); + assertTrue(EnvName.equals("foo.\"bar\".baz", new String("FOO__BAR__BAZ"))); + assertTrue(envSourceEquals("FOO__BAR__BAZ", new String("foo.\"bar\".baz"))); + assertTrue(EnvName.equals("FOO__BAR__BAZ_0__Z_0_", new String("foo.\"bar\".baz[0].z[0]"))); + assertTrue(envSourceEquals("FOO__BAR__BAZ_0__Z_0_", new String("foo.\"bar\".baz[0].z[0]"))); + + assertTrue(EnvName.equals("_DEV_FOO_BAR", new String("%dev.foo.bar"))); + assertTrue(EnvName.equals("%dev.foo.bar", new String("_DEV_FOO_BAR"))); + assertTrue(envSourceEquals("_DEV_FOO_BAR", new String("%dev.foo.bar"))); + assertTrue(EnvName.equals("_ENV_SMALLRYE_MP_CONFIG_PROP", new String("_ENV_SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(EnvName.equals("%env.smallrye.mp.config.prop", new String("%env.smallrye.mp.config.prop"))); + assertTrue(EnvName.equals("_ENV_SMALLRYE_MP_CONFIG_PROP", new String("%env.smallrye.mp.config.prop"))); + assertTrue(EnvName.equals("%env.smallrye.mp.config.prop", new String("_ENV_SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("%env.smallrye.mp.config.prop", new String("%env.smallrye.mp.config.prop"))); + assertTrue(envSourceEquals("_ENV_SMALLRYE_MP_CONFIG_PROP", new String("%env.smallrye.mp.config.prop"))); + + assertTrue(EnvName.equals("indexed[0]", new String("indexed[0]"))); + assertTrue(EnvName.equals("INDEXED_0_", new String("INDEXED_0_"))); + assertTrue(EnvName.equals("indexed[0]", new String("INDEXED_0_"))); + assertTrue(EnvName.equals("INDEXED_0_", new String("indexed[0]"))); + assertTrue(envSourceEquals("indexed[0]", new String("indexed[0]"))); + assertTrue(envSourceEquals("INDEXED_0_", new String("INDEXED_0_"))); + assertTrue(envSourceEquals("INDEXED_0_", new String("indexed[0]"))); + assertTrue(envSourceEquals("foo.bar.indexed[0]", new String("foo.bar.indexed[0]"))); + assertTrue(envSourceEquals("FOO_BAR_INDEXED_0_", new String("foo.bar.indexed[0]"))); + assertTrue(envSourceEquals("foo.bar[0].indexed[0]", new String("foo.bar[0].indexed[0]"))); + assertTrue(envSourceEquals("FOO_BAR_0__INDEXED_0_", new String("foo.bar[0].indexed[0]"))); + + assertTrue(EnvName.equals("env.\"quoted.key\".value", new String("env.\"quoted.key\".value"))); + assertTrue(EnvName.equals("ENV__QUOTED_KEY__VALUE", new String("ENV__QUOTED_KEY__VALUE"))); + assertTrue(EnvName.equals("ENV__QUOTED_KEY__VALUE", new String("env.\"quoted.key\".value"))); + assertTrue(EnvName.equals("env.\"quoted.key\".value", new String("ENV__QUOTED_KEY__VALUE"))); + assertTrue(EnvName.equals("env.\"quoted.key\".value", new String("env.\"quoted-key\".value"))); + assertTrue(EnvName.equals("env.\"quoted-key\".value", new String("env.\"quoted.key\".value"))); + assertTrue(envSourceEquals("env.\"quoted.key\".value", new String("env.\"quoted.key\".value"))); + assertTrue(envSourceEquals("ENV__QUOTED_KEY__VALUE", new String("ENV__QUOTED_KEY__VALUE"))); + assertTrue(envSourceEquals("ENV__QUOTED_KEY__VALUE", new String("env.\"quoted.key\".value"))); + assertTrue(envSourceEquals("env.\"quoted.key\".value", new String("env.\"quoted-key\".value"))); + assertTrue(envSourceEquals("env.\"quoted-key\".value", new String("env.\"quoted.key\".value"))); + + assertTrue(EnvName.equals("TEST_LANGUAGE__DE_ETR__", new String("test.language.\"de.etr\""))); + assertTrue(EnvName.equals("test.language.\"de.etr\"", new String("TEST_LANGUAGE__DE_ETR__"))); + assertEquals(new EnvName("TEST_LANGUAGE__DE_ETR_").hashCode(), new EnvName("test.language.\"de.etr\"").hashCode()); + + assertTrue(envSourceEquals("SMALLRYE_MP_CONFIG_PROP", new String("smallrye/mp/config/prop"))); + assertTrue(envSourceEquals("__SMALLRYE", new String("$$smallrye"))); + assertTrue(envSourceEquals("$$smallrye", new String("__SMALLRYE"))); + + assertTrue(envSourceEquals("__SMALLRYE_MP_CONFIG_PROP", new String("$$SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("&&SMALLRYE_MP_CONFIG_PROP", new String("__SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("__SMALLRYE_MP_CONFIG_PROP", new String("$$SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("$$SMALLRYE_MP_CONFIG_PROP", new String("__SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("__SMALLRYE_MP_CONFIG_PROP", new String("__SMALLRYE$MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("__SMALLRYE$MP_CONFIG_PROP", new String("__SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("__SMALLRYE_MP_CONFIG_PROP", new String("&&SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("&&SMALLRYE_MP_CONFIG_PROP", new String("__SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("__SMALLRYE_MP_CONFIG_PROP", new String("##SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("##SMALLRYE_MP_CONFIG_PROP", new String("__SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("__SMALLRYE_MP_CONFIG_PROP", new String("!!SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("!!SMALLRYE_MP_CONFIG_PROP", new String("__SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("__SMALLRYE_MP_CONFIG_PROP", new String("++SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("++SMALLRYE_MP_CONFIG_PROP", new String("__SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("__SMALLRYE_MP_CONFIG_PROP", new String("??SMALLRYE_MP_CONFIG_PROP"))); + assertTrue(envSourceEquals("??SMALLRYE_MP_CONFIG_PROP", new String("__SMALLRYE_MP_CONFIG_PROP"))); + } + + @Test + void sameSemanticMeaning() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "foo.bar-baz", "fromOther", + "%dev.foo.bar-devbaz", "fromOther", + "foo.bar-commonbaz", "fromOther")) + .withSources(new EnvConfigSource(Map.of( + "FOO_BAR_BAZ", "fromEnv", + "FOO_BAR_DEVBAZ", "fromEnv", + "_COMMON_FOO_BAR_COMMONBAZ", "fromEnv"), 300)) + .withProfiles(List.of("dev", "common")) + .build(); + + Set properties = stream(config.getPropertyNames().spliterator(), false).collect(toSet()); + assertTrue(properties.contains("FOO_BAR_BAZ")); + assertTrue(properties.contains("foo.bar-baz")); + assertFalse(properties.contains("foo.bar.baz")); + assertEquals("fromEnv", config.getRawValue("foo.bar-baz")); + + assertTrue(properties.contains("FOO_BAR_DEVBAZ")); + assertTrue(properties.contains("foo.bar-devbaz")); + assertFalse(properties.contains("foo.bar.devbaz")); + assertEquals("fromEnv", config.getRawValue("foo.bar-devbaz")); + + assertTrue(properties.contains("foo.bar-commonbaz")); + assertEquals("fromEnv", config.getRawValue("foo.bar-commonbaz")); + } + + @Test + void sameNames() { + assertTrue(envSourceEquals("FOOBAR", "foobar")); + assertTrue(envSourceEquals("FOOBAR", "fooBar")); + + EnvConfigSource envConfigSource = new EnvConfigSource( + Map.of("my_string_property", "lower", "MY_STRING_PROPERTY", "upper"), 100); + assertEquals(2, envConfigSource.getProperties().size()); + assertEquals("lower", envConfigSource.getValue("my_string_property")); + assertEquals("upper", envConfigSource.getValue("MY_STRING_PROPERTY")); + } + + @Test + void dashedEnvNames() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(DashedEnvNames.class) + .withSources(new EnvConfigSource(Map.of( + "DASHED_ENV_NAMES_VALUE", "value", + "DASHED_ENV_NAMES_NESTED__DASHED_KEY__ANOTHER", "value"), 100)) + .build(); + + DashedEnvNames mapping = config.getConfigMapping(DashedEnvNames.class); + + assertEquals("value", mapping.value()); + // Unfortunately, we still don't have a good way to determine if the Map key is dashed or not + assertEquals("value", mapping.nested().get("dashed.key").another()); + } + + @Test + void dottedDashedEnvNames() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(DashedEnvNames.class) + .withSources(new EnvConfigSource(Map.of( + "dashed-env-names.value", "value", + "dashed-env-names.nested.dashed-key.another", "value"), 100)) + .build(); + + DashedEnvNames mapping = config.getConfigMapping(DashedEnvNames.class); + + assertEquals("value", mapping.value()); + assertEquals("value", mapping.nested().get("dashed-key").another()); + } + + @ConfigMapping(prefix = "dashed-env-names") + interface DashedEnvNames { + String value(); + + Map nested(); + + interface Nested { + String another(); + } + } + + @Test + void ignoreUnmappedWithMappingIgnore() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMappingIgnore("ignore.**") + .withMapping(IgnoreUnmappedWithMappingIgnore.class) + .withSources(new EnvConfigSource(Map.of( + "IGNORE_VALUE", "value", + "IGNORE_LIST_0_", "0", + "IGNORE_NESTED_VALUE", "nested", + "IGNORE_IGNORE", "ignore", + "IGNORE_NESTED_IGNORE", "ignore"), 100)) + .build(); + + IgnoreUnmappedWithMappingIgnore mapping = config.getConfigMapping(IgnoreUnmappedWithMappingIgnore.class); + + assertEquals("value", mapping.value()); + assertEquals(0, mapping.list().get(0)); + assertEquals("nested", mapping.nested().value()); + } + + @ConfigMapping(prefix = "ignore") + interface IgnoreUnmappedWithMappingIgnore { + String value(); + + List list(); + + Nested nested(); + + interface Nested { + String value(); + } + } + + @Test + void unmappedProfile() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(UnmappedProfile.class) + .withSources(new EnvConfigSource(Map.of( + "_DEV_UNMAPPED_A_VALUE", "value", + "_DEV_UNMAPPED_NESTED_TYPE_VALUE", "value"), 100)) + .withProfile("dev") + .build(); + + UnmappedProfile mapping = config.getConfigMapping(UnmappedProfile.class); + assertTrue(mapping.aValue().isPresent()); + assertEquals("value", mapping.aValue().get()); + assertEquals("value", mapping.nestedType().value()); + + config = new SmallRyeConfigBuilder() + .withMapping(UnmappedProfile.class) + .withSources(new EnvConfigSource(Map.of( + "_DEV_UNMAPPED_A_VALUE", "value", + "_DEV_UNMAPPED_NESTED_TYPE_VALUE", "value"), 200)) + .withSources(config( + "%dev.unmapped.nested-type.value", "value")) + .withSources(config( + "unmapped.a.value", "value", + "unmapped.nested.type.value", "value")) + .withProfile("dev") + .build(); + + mapping = config.getConfigMapping(UnmappedProfile.class); + assertTrue(mapping.aValue().isPresent()); + assertEquals("value", mapping.aValue().get()); + assertEquals("value", mapping.nestedType().value()); + + SmallRyeConfigBuilder builder = new SmallRyeConfigBuilder() + .withMapping(UnmappedProfile.class) + .withSources(new EnvConfigSource(Map.of( + "_DEV_UNMAPPED_A_VALUE", "value", + "_DEV_UNMAPPED_NESTED_TYPE_VALUE", "value", + "_DEV_UNMAPPED_UNMAPPED", "value"), 200)) + .withSources(config( + "%dev.unmapped.nested-type.value", "value")) + .withSources(config( + "unmapped.a.value", "value", + "unmapped.nested.type.value", "value")) + .withProfile("dev"); + + // TODO - https://github.com/quarkusio/quarkus/issues/38479 + // ConfigValidationException exception = assertThrows(ConfigValidationException.class, builder::build); + // assertEquals("SRCFG00050: unmapped.unmapped in EnvConfigSource does not map to any root", + // exception.getProblem(0).getMessage()); + } + + @ConfigMapping(prefix = "unmapped") + interface UnmappedProfile { + Optional aValue(); + + Nested nestedType(); + + interface Nested { + String value(); + } + } + + @Test + void ignoreUnmappedWithMap() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(IgnoreUnmappedWithMap.class) + .withSources(new EnvConfigSource(Map.of( + "IGNORE_VALUE", "value", + "IGNORE_LIST_0_", "0", + "IGNORE_NESTED_VALUE", "nested", + "IGNORE_IGNORE", "ignore", + "IGNORE_NESTED_IGNORE", "ignore"), 100)) + .build(); + + IgnoreUnmappedWithMap mapping = config.getConfigMapping(IgnoreUnmappedWithMap.class); + + assertEquals("value", mapping.value()); + assertEquals(0, mapping.list().get(0)); + assertEquals("nested", mapping.nested().value()); + } + + @ConfigMapping(prefix = "ignore") + interface IgnoreUnmappedWithMap { + String value(); + + List list(); + + Nested nested(); + + @WithParentName + Map ignore(); + + interface Nested { + String value(); + } + } + + @Test + void mappingFactory() { + ConfigSourceFactory.ConfigurableConfigSourceFactory sourceFactory = new ConfigSourceFactory.ConfigurableConfigSourceFactory<>() { + @Override + public Iterable getConfigSources(final ConfigSourceContext context, final MappingFactory mapping) { + assertEquals("value", mapping.aValue()); + assertEquals("value", mapping.aMap().get("key")); + return emptyList(); + } + }; + + new SmallRyeConfigBuilder() + .withSources(new EnvConfigSource(Map.of( + "MAPPING_A_VALUE", "value", + "MAPPING_A_MAP_KEY", "value"), 300)) + .withSources(sourceFactory) + .build(); + } + + @ConfigMapping(prefix = "mapping") + interface MappingFactory { + String aValue(); + + Map aMap(); + } + + private static boolean envSourceEquals(String name, String lookup) { + return BOOLEAN_CONVERTER.convert(new EnvConfigSource(Map.of(name, "true"), 100).getValue(lookup)); } } diff --git a/implementation/src/test/java/io/smallrye/config/ExpressionConfigSourceInterceptorTest.java b/implementation/src/test/java/io/smallrye/config/ExpressionConfigSourceInterceptorTest.java index 325c49919..6ca5abf49 100644 --- a/implementation/src/test/java/io/smallrye/config/ExpressionConfigSourceInterceptorTest.java +++ b/implementation/src/test/java/io/smallrye/config/ExpressionConfigSourceInterceptorTest.java @@ -2,13 +2,18 @@ import static java.util.stream.Collectors.toList; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.stream.Stream; +import jakarta.annotation.Priority; + import org.junit.jupiter.api.Test; class ExpressionConfigSourceInterceptorTest { @@ -77,7 +82,7 @@ void noExpression() { void noExpressionComposed() { SmallRyeConfig config = buildConfig("expression", "${my.prop${compose}}"); - final NoSuchElementException exception = assertThrows(NoSuchElementException.class, + NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> config.getValue("expression", String.class)); assertEquals("SRCFG00011: Could not expand value compose in property expression", exception.getMessage()); } @@ -107,8 +112,6 @@ void withoutExpansion() { assertEquals("1234", config.getValue("expression", String.class)); - Expressions.withoutExpansion(() -> assertEquals("${my.prop}", config.getValue("expression", String.class))); - Expressions.withoutExpansion(() -> assertEquals("${my.prop}", config.getValue("expression", String.class))); Expressions.withoutExpansion(() -> assertEquals("${my.prop}", config.getValue("expression", String.class))); assertEquals("1234", config.getValue("expression", String.class)); @@ -122,53 +125,119 @@ void escape() { assertEquals("file:target/prices/?fileName=${date:now:yyyyMMddssSS}.txt&charset=utf-8", buildConfig("camel.expression", "file:target/prices/?fileName=$${date:now:yyyyMMddssSS}.txt&charset=utf-8") - .getRawValue("camel.expression")); + .getRawValue("camel.expression")); assertEquals("file:target/prices/?fileName=${date:now:yyyyMMddssSS}.txt&charset=utf-8", buildConfig("camel.expression", "file:target/prices/?fileName=\\${date:now:yyyyMMddssSS}.txt&charset=utf-8") - .getRawValue("camel.expression")); + .getRawValue("camel.expression")); } @Test void expressionMissing() { - final SmallRyeConfig config = buildConfig("my.prop", "${expression}", "my.prop.partial", "${expression}partial"); + SmallRyeConfig config = buildConfig("my.prop", "${expression}", "my.prop.partial", "${expression}partial"); + + assertThrows(Exception.class, () -> config.getValue("my.prop", String.class)); + assertThrows(Exception.class, () -> config.getValue("my.prop.partial", String.class)); + } + + @Test + void expressionMissingOptional() { + SmallRyeConfig config = buildConfig("my.prop", "${expression}", + "my.prop.partial", "${expression}partial", + "my.prop.anotherPartial", "par${expression}tial", + "my.prop.dependent", "${my.prop.partial}"); + + assertEquals(Optional.empty(), config.getOptionalValue("my.prop", String.class)); + assertEquals(Optional.empty(), config.getOptionalValue("my.prop.partial", String.class)); + assertEquals(Optional.empty(), config.getOptionalValue("my.prop.anotherPartial", String.class)); + assertEquals(Optional.empty(), config.getOptionalValue("my.prop.dependent", String.class)); + + ConfigValue noExpression = config.getConfigValue("my.prop"); + assertNotNull(noExpression); + assertEquals(noExpression.getName(), "my.prop"); + assertNull(noExpression.getValue()); + + ConfigValue noExpressionPartial = config.getConfigValue("my.prop.partial"); + assertNotNull(noExpressionPartial); + assertEquals(noExpressionPartial.getName(), "my.prop.partial"); + assertNull(noExpressionPartial.getValue()); + + ConfigValue noExpressionAnotherPartial = config.getConfigValue("my.prop.anotherPartial"); + assertNotNull(noExpressionAnotherPartial); + assertEquals(noExpressionAnotherPartial.getName(), "my.prop.anotherPartial"); + assertNull(noExpressionAnotherPartial.getValue()); + + ConfigValue noExpressionDependent = config.getConfigValue("my.prop.dependent"); + assertNotNull(noExpressionDependent); + assertEquals(noExpressionDependent.getName(), "my.prop.dependent"); + assertNull(noExpressionDependent.getValue()); assertThrows(Exception.class, () -> config.getValue("my.prop", String.class)); assertThrows(Exception.class, () -> config.getValue("my.prop.partial", String.class)); + assertThrows(Exception.class, () -> config.getValue("my.prop.anotherPartial", String.class)); + assertThrows(Exception.class, () -> config.getValue("my.prop.dependent", String.class)); } @Test void arrayEscapes() { - final SmallRyeConfig config = buildConfig("list", "cat,dog,${mouse},sea\\,turtle", "mouse", "mouse"); - final List list = config.getValues("list", String.class, ArrayList::new); + SmallRyeConfig config = buildConfig("list", "cat,dog,${mouse},sea\\,turtle", "mouse", "mouse"); + List list = config.getValues("list", String.class, ArrayList::new); assertEquals(4, list.size()); assertEquals(list, Stream.of("cat", "dog", "mouse", "sea,turtle").collect(toList())); } @Test void escapeDollar() { - final SmallRyeConfig config = buildConfig("my.prop", "\\${value\\${another}end:value}"); + SmallRyeConfig config = buildConfig("my.prop", "\\${value\\${another}end:value}"); assertEquals("${value${another}end:value}", config.getRawValue("my.prop")); } @Test void escapeBraces() { - final SmallRyeConfig config = buildConfig("my.prop", "${value:111{111}"); + SmallRyeConfig config = buildConfig("my.prop", "${value:111{111}"); assertEquals("111{111", config.getRawValue("my.prop")); } @Test void windowPath() { - final SmallRyeConfig config = buildConfig("window.path", "C:\\Some\\Path"); + SmallRyeConfig config = buildConfig("window.path", "C:\\Some\\Path"); assertEquals("C:\\Some\\Path", config.getRawValue("window.path")); } + @Test + void nullValue() { + SmallRyeConfig config = buildConfigWithCustomInterceptor("sth", null); + ConfigValue configValue = config.getConfigValue("sth"); + + assertNotNull(configValue); + + // No exception is thrown, only null is returned + assertNull(configValue.getValue()); + } + private static SmallRyeConfig buildConfig(String... keyValues) { return new SmallRyeConfigBuilder() - .addDefaultSources() + .addDefaultInterceptors() .withSources(KeyValuesConfigSource.config(keyValues)) + .build(); + } + + private static SmallRyeConfig buildConfigWithCustomInterceptor(String... keyValues) { + return new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withInterceptors(new CustomConfigSourceInterceptor()) .withInterceptors(new ExpressionConfigSourceInterceptor()) + .withSources(KeyValuesConfigSource.config(keyValues)) .build(); } + + @Priority(Priorities.LIBRARY + 201) + private static class CustomConfigSourceInterceptor implements ConfigSourceInterceptor { + + @Override + public ConfigValue getValue(ConfigSourceInterceptorContext context, String name) { + return ConfigValue.builder().withName(name).withValue(null).build(); + } + } } diff --git a/implementation/src/test/java/io/smallrye/config/ImplicitConverterTest.java b/implementation/src/test/java/io/smallrye/config/ImplicitConverterTest.java index e9188dae6..792f1365f 100644 --- a/implementation/src/test/java/io/smallrye/config/ImplicitConverterTest.java +++ b/implementation/src/test/java/io/smallrye/config/ImplicitConverterTest.java @@ -14,7 +14,6 @@ import java.net.URL; import java.time.LocalDate; -import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.Converter; import org.junit.jupiter.api.Test; @@ -23,8 +22,9 @@ class ImplicitConverterTest { @Test void implicitURLConverter() { - Config config = buildConfig( - "my.url", "https://github.com/smallrye/smallrye-config/"); + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(KeyValuesConfigSource.config("my.url", "https://github.com/smallrye/smallrye-config/")) + .build(); URL url = config.getValue("my.url", URL.class); assertNotNull(url); assertEquals("https", url.getProtocol()); @@ -34,8 +34,9 @@ void implicitURLConverter() { @Test void implicitLocalDateConverter() { - Config config = buildConfig( - "my.date", "2019-04-01"); + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(KeyValuesConfigSource.config("my.date", "2019-04-01")) + .build(); LocalDate date = config.getValue("my.date", LocalDate.class); assertNotNull(date); assertEquals(2019, date.getYear()); @@ -81,39 +82,119 @@ enum MyOtherEnum { @Test public void convertMyEnum() { - HyphenateEnumConverter hyphenateEnumConverter = new HyphenateEnumConverter<>(MyEnum.class); - assertEquals(hyphenateEnumConverter.convert("DISCARD"), MyEnum.DISCARD); - assertEquals(hyphenateEnumConverter.convert("discard"), MyEnum.DISCARD); - assertEquals(hyphenateEnumConverter.convert("READ_UNCOMMITTED"), MyEnum.READ_UNCOMMITTED); - assertEquals(hyphenateEnumConverter.convert("a-b"), MyEnum.A_B); - assertEquals(hyphenateEnumConverter.convert("read-uncommitted"), MyEnum.READ_UNCOMMITTED); - assertEquals(hyphenateEnumConverter.convert("SIGUSR1"), MyEnum.SIGUSR1); - assertEquals(hyphenateEnumConverter.convert("sigusr1"), MyEnum.SIGUSR1); - assertEquals(hyphenateEnumConverter.convert("TrendBreaker"), MyEnum.TrendBreaker); - assertEquals(hyphenateEnumConverter.convert("trend-breaker"), MyEnum.TrendBreaker); - assertEquals(hyphenateEnumConverter.convert("MAKING_LifeDifficult"), MyEnum.MAKING_LifeDifficult); - //assertEquals(hyphenateEnumConverter.convert("making-life-difficult"), MyEnum.MAKING_LifeDifficult); + HyphenateEnumConverter converter = new HyphenateEnumConverter<>(MyEnum.class); + assertEquals(MyEnum.DISCARD, converter.convert("DISCARD")); + assertEquals(MyEnum.DISCARD, converter.convert("discard")); + assertEquals(MyEnum.READ_UNCOMMITTED, converter.convert("READ_UNCOMMITTED")); + assertEquals(MyEnum.A_B, converter.convert("a-b")); + assertEquals(MyEnum.READ_UNCOMMITTED, converter.convert("read-uncommitted")); + assertEquals(MyEnum.SIGUSR1, converter.convert("SIGUSR1")); + assertEquals(MyEnum.SIGUSR1, converter.convert("sigusr1")); + assertEquals(MyEnum.TrendBreaker, converter.convert("TrendBreaker")); + assertEquals(MyEnum.TrendBreaker, converter.convert("trend-breaker")); + assertEquals(MyEnum.MAKING_LifeDifficult, converter.convert("MAKING_LifeDifficult")); + assertEquals(MyEnum.MAKING_LifeDifficult, converter.convert("making-life-difficult")); } @Test public void convertMyOtherEnum() { - HyphenateEnumConverter hyphenateEnumConverter = new HyphenateEnumConverter<>(MyOtherEnum.class); - assertEquals(hyphenateEnumConverter.convert("makingLifeDifficult"), MyOtherEnum.makingLifeDifficult); - assertEquals(hyphenateEnumConverter.convert("making-life-difficult"), MyOtherEnum.makingLifeDifficult); - assertEquals(hyphenateEnumConverter.convert("READ__UNCOMMITTED"), MyOtherEnum.READ__UNCOMMITTED); - //assertEquals(hyphenateEnumConverter.convert("read-uncommitted"), MyOtherEnum.READ__UNCOMMITTED); + HyphenateEnumConverter converter = new HyphenateEnumConverter<>(MyOtherEnum.class); + assertEquals(MyOtherEnum.makingLifeDifficult, converter.convert("makingLifeDifficult")); + assertEquals(MyOtherEnum.makingLifeDifficult, converter.convert("making-life-difficult")); + assertEquals(MyOtherEnum.READ__UNCOMMITTED, converter.convert("READ__UNCOMMITTED")); + assertEquals(MyOtherEnum.READ__UNCOMMITTED, converter.convert("read-uncommitted")); } @Test - public void testIllegalEnumConfigUtilConversion() { + public void illegalEnumConfigUtilConversion() { HyphenateEnumConverter hyphenateEnumConverter = new HyphenateEnumConverter<>(MyEnum.class); - assertThrows(IllegalArgumentException.class, () -> hyphenateEnumConverter.convert("READUNCOMMITTED")); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> hyphenateEnumConverter.convert("READUNCOMMITTED")); + assertEquals( + "SRCFG00049: Cannot convert READUNCOMMITTED to enum class io.smallrye.config.ImplicitConverterTest$MyEnum, " + + "allowed values: trend-breaker,making-life-difficult,discard,sigusr1,read-uncommitted,a-b", + exception.getMessage()); } - private static Config buildConfig(String... keyValues) { - return new SmallRyeConfigBuilder() - .addDefaultSources() - .withSources(KeyValuesConfigSource.config(keyValues)) - .build(); + public enum KeyEncryptionAlgorithm { + RSA_OAEP, + RSA_OAEP_256, + ECDH_ES, + ECDH_ES_A128KW, + ECDH_ES_A192KW, + ECDH_ES_A256KW, + A128KW, + A192KW, + A256KW, + A128GCMKW, + A192GCMKW, + A256GCMKW, + PBES2_HS256_A128KW, + PBES2_HS384_A192KW, + PBES2_HS512_A256KW; + } + + @Test + void convertKeyEncryptionAlgorithm() { + HyphenateEnumConverter converter = new HyphenateEnumConverter<>(KeyEncryptionAlgorithm.class); + assertEquals(KeyEncryptionAlgorithm.RSA_OAEP, converter.convert("RSA_OAEP")); + assertEquals(KeyEncryptionAlgorithm.RSA_OAEP, converter.convert("RSA-OAEP")); + assertEquals(KeyEncryptionAlgorithm.RSA_OAEP, converter.convert("rsa-oaep")); + assertEquals(KeyEncryptionAlgorithm.RSA_OAEP_256, converter.convert("RSA_OAEP_256")); + assertEquals(KeyEncryptionAlgorithm.RSA_OAEP_256, converter.convert("RSA-OAEP-256")); + assertEquals(KeyEncryptionAlgorithm.RSA_OAEP_256, converter.convert("rsa-oaep-256")); + assertEquals(KeyEncryptionAlgorithm.ECDH_ES, converter.convert("ECDH_ES")); + assertEquals(KeyEncryptionAlgorithm.ECDH_ES, converter.convert("ECDH-ES")); + assertEquals(KeyEncryptionAlgorithm.ECDH_ES, converter.convert("ecdh-es")); + assertEquals(KeyEncryptionAlgorithm.ECDH_ES_A128KW, converter.convert("ECDH_ES_A128KW")); + assertEquals(KeyEncryptionAlgorithm.ECDH_ES_A128KW, converter.convert("ECDH-ES-A128KW")); + assertEquals(KeyEncryptionAlgorithm.ECDH_ES_A128KW, converter.convert("ecdh-es-a128kw")); + assertEquals(KeyEncryptionAlgorithm.ECDH_ES_A192KW, converter.convert("ECDH_ES_A192KW")); + assertEquals(KeyEncryptionAlgorithm.ECDH_ES_A192KW, converter.convert("ECDH-ES-A192KW")); + assertEquals(KeyEncryptionAlgorithm.ECDH_ES_A192KW, converter.convert("ecdh-es-a192kw")); + assertEquals(KeyEncryptionAlgorithm.ECDH_ES_A256KW, converter.convert("ECDH_ES_A256KW")); + assertEquals(KeyEncryptionAlgorithm.ECDH_ES_A256KW, converter.convert("ECDH-ES-A256KW")); + assertEquals(KeyEncryptionAlgorithm.ECDH_ES_A256KW, converter.convert("ecdh-es-a256kw")); + assertEquals(KeyEncryptionAlgorithm.A128KW, converter.convert("A128KW")); + assertEquals(KeyEncryptionAlgorithm.A128KW, converter.convert("a128kw")); + assertEquals(KeyEncryptionAlgorithm.A192KW, converter.convert("A192KW")); + assertEquals(KeyEncryptionAlgorithm.A192KW, converter.convert("a192kw")); + assertEquals(KeyEncryptionAlgorithm.A256KW, converter.convert("A256KW")); + assertEquals(KeyEncryptionAlgorithm.A256KW, converter.convert("a256kw")); + assertEquals(KeyEncryptionAlgorithm.A128GCMKW, converter.convert("A128GCMKW")); + assertEquals(KeyEncryptionAlgorithm.A128GCMKW, converter.convert("a128gcmkw")); + assertEquals(KeyEncryptionAlgorithm.A192GCMKW, converter.convert("A192GCMKW")); + assertEquals(KeyEncryptionAlgorithm.A192GCMKW, converter.convert("a192gcmkw")); + assertEquals(KeyEncryptionAlgorithm.A256GCMKW, converter.convert("A256GCMKW")); + assertEquals(KeyEncryptionAlgorithm.A256GCMKW, converter.convert("a256gcmkw")); + assertEquals(KeyEncryptionAlgorithm.PBES2_HS256_A128KW, converter.convert("PBES2_HS256_A128KW")); + assertEquals(KeyEncryptionAlgorithm.PBES2_HS256_A128KW, converter.convert("PBES2-HS256-A128KW")); + assertEquals(KeyEncryptionAlgorithm.PBES2_HS256_A128KW, converter.convert("pbes2-hs256-a128kw")); + assertEquals(KeyEncryptionAlgorithm.PBES2_HS384_A192KW, converter.convert("PBES2_HS384_A192KW")); + assertEquals(KeyEncryptionAlgorithm.PBES2_HS384_A192KW, converter.convert("PBES2-HS384-A192KW")); + assertEquals(KeyEncryptionAlgorithm.PBES2_HS384_A192KW, converter.convert("pbes2-hs384-a192kw")); + assertEquals(KeyEncryptionAlgorithm.PBES2_HS512_A256KW, converter.convert("PBES2_HS512_A256KW")); + assertEquals(KeyEncryptionAlgorithm.PBES2_HS512_A256KW, converter.convert("PBES2-HS512-A256KW")); + assertEquals(KeyEncryptionAlgorithm.PBES2_HS512_A256KW, converter.convert("pbes2-hs512-a256kw")); + } + + enum HTTPConduit { + QuarkusCXFDefault, + CXFDefault, + HttpClientHTTPConduitFactory, + URLConnectionHTTPConduitFactory + } + + @Test + void convertHttpConduit() { + HyphenateEnumConverter converter = new HyphenateEnumConverter<>(HTTPConduit.class); + assertEquals(HTTPConduit.QuarkusCXFDefault, converter.convert("QuarkusCXFDefault")); + assertEquals(HTTPConduit.QuarkusCXFDefault, converter.convert("quarkus-cxf-default")); + assertEquals(HTTPConduit.CXFDefault, converter.convert("CXFDefault")); + assertEquals(HTTPConduit.CXFDefault, converter.convert("cxf-default")); + assertEquals(HTTPConduit.HttpClientHTTPConduitFactory, converter.convert("HttpClientHTTPConduitFactory")); + assertEquals(HTTPConduit.HttpClientHTTPConduitFactory, converter.convert("http-client-http-conduit-factory")); + assertEquals(HTTPConduit.URLConnectionHTTPConduitFactory, converter.convert("URLConnectionHTTPConduitFactory")); + assertEquals(HTTPConduit.URLConnectionHTTPConduitFactory, converter.convert("url-connection-http-conduit-factory")); } } diff --git a/implementation/src/test/java/io/smallrye/config/KeyMapTest.java b/implementation/src/test/java/io/smallrye/config/KeyMapTest.java index 8d2bc7e82..492020f84 100644 --- a/implementation/src/test/java/io/smallrye/config/KeyMapTest.java +++ b/implementation/src/test/java/io/smallrye/config/KeyMapTest.java @@ -3,10 +3,12 @@ import static java.util.stream.Collectors.toList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayDeque; import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -98,14 +100,6 @@ void merge() { assertNull(map.findRootValue("root.foo.bar.y.baz.z")); } - @Test - void empty() { - KeyMap map = new KeyMap<>(); - map.findOrAdd("", "foo").putRootValue("bar"); - - assertEquals("bar", map.findRootValue(".foo")); - } - @Test void string() { KeyMap map = new KeyMap<>(); @@ -202,7 +196,112 @@ void putAllAny() { void map() { KeyMap map = new KeyMap<>(); map.findOrAdd("map.roles.*[*]").putRootValue("foo"); + map.findOrAdd("map.threadpool.config[customPool].id").putRootValue("customPool"); assertEquals("foo", map.findRootValue("map.roles.user[0]")); + assertEquals("customPool", map.findRootValue("map.threadpool.config[customPool].id")); + } + + @Test + void findOrAdd() { + KeyMap varArgs = new KeyMap<>(); + varArgs.findOrAdd("foo", "bar", "baz").putRootValue("value"); + + KeyMap array = new KeyMap<>(); + array.findOrAdd(new String[] { "foo", "bar", "baz" }, 0, 3).putRootValue("value"); + + KeyMap string = new KeyMap<>(); + string.findOrAdd("foo.bar.baz").putRootValue("value"); + + KeyMap nameIterator = new KeyMap<>(); + nameIterator.findOrAdd(new NameIterator("foo.bar.baz")).putRootValue("value"); + + KeyMap iterator = new KeyMap<>(); + iterator.findOrAdd(Stream.of("foo", "bar", "baz").collect(toList()).iterator()).putRootValue("value"); + + KeyMap iterable = new KeyMap<>(); + iterable.findOrAdd(Stream.of("foo", "bar", "baz").collect(toList())).putRootValue("value"); + + assertEquals("value", varArgs.findRootValue("foo.bar.baz")); + assertEquals("value", array.findRootValue("foo.bar.baz")); + assertEquals("value", string.findRootValue("foo.bar.baz")); + assertEquals("value", nameIterator.findRootValue("foo.bar.baz")); + assertEquals("value", iterator.findRootValue("foo.bar.baz")); + assertEquals("value", iterable.findRootValue("foo.bar.baz")); + } + + @Test + void findOrAddDotted() { + KeyMap map = new KeyMap<>(); + map.findOrAdd("map.\"quoted.key\".value").putRootValue("value"); + assertEquals("value", map.findRootValue("map.\"quoted.key\".value")); + assertNull(map.findRootValue("map.quoted.key.value")); + + KeyMap varArgs = new KeyMap<>(); + varArgs.findOrAdd("foo", "bar.bar", "baz").putRootValue("value"); + + KeyMap array = new KeyMap<>(); + array.findOrAdd(new String[] { "foo", "bar.bar", "baz" }, 0, 3).putRootValue("value"); + + KeyMap string = new KeyMap<>(); + string.findOrAdd("foo.\"bar.bar\".baz").putRootValue("value"); + + KeyMap nameIterator = new KeyMap<>(); + nameIterator.findOrAdd(new NameIterator("foo.\"bar.bar\".baz")).putRootValue("value"); + + KeyMap iterator = new KeyMap<>(); + iterator.findOrAdd(Stream.of("foo", "bar.bar", "baz").collect(toList()).iterator()).putRootValue("value"); + + KeyMap iterable = new KeyMap<>(); + iterable.findOrAdd(Stream.of("foo", "bar.bar", "baz").collect(toList())).putRootValue("value"); + + assertEquals("value", varArgs.findRootValue("foo.\"bar.bar\".baz")); + assertEquals("value", array.findRootValue("foo.\"bar.bar\".baz")); + assertEquals("value", string.findRootValue("foo.\"bar.bar\".baz")); + assertEquals("value", nameIterator.findRootValue("foo.\"bar.bar\".baz")); + assertEquals("value", iterator.findRootValue("foo.\"bar.bar\".baz")); + assertEquals("value", iterable.findRootValue("foo.\"bar.bar\".baz")); + } + + @Test + void star() { + KeyMap map = new KeyMap<>(); + KeyMap orAdd = map.findOrAdd("map.key.*"); + orAdd.putRootValue("value"); + orAdd.putAny(map.findOrAdd("map.key.*")); + + assertEquals("value", map.findRootValue("map.key.one")); + assertEquals("value", map.findRootValue("map.key.one[1]")); + assertEquals("value", map.findRootValue("map.key.one.two")); + assertEquals("value", map.findRootValue("map.key.one.two.three")); + } + + @Test + void keySet() { + KeyMap map = new KeyMap<>(); + map.findOrAdd("root.foo").putRootValue("bar"); + map.findOrAdd("root.foo.bar").putRootValue("baz"); + map.findOrAdd("root.foo.bar.*").putRootValue("baz"); + map.findOrAdd("root.foo.bar.*.baz").putRootValue("anything"); + map.findOrAdd("list[0]").putRootValue("bar"); + map.findOrAdd("list[0].foo").putRootValue("bar"); + + assertEquals("bar", map.findRootValue("list[0].foo")); + assertEquals("bar", map.findRootValue("list[0]")); + + Set keys = map.keySet(); + assertEquals(4, keys.size()); + assertTrue(keys.contains("root.foo")); + assertTrue(keys.contains("root.foo.bar")); + assertTrue(keys.contains("list[0].foo")); + assertTrue(keys.contains("list[0]")); + } + + @Test + void emptySegment() { + KeyMap map = new KeyMap<>(); + map.findOrAdd("root.foo.*").putRootValue("bar"); + assertEquals("bar", map.findRootValue("root.foo.bar")); + assertNull(map.findRootValue("root.foo.")); } } diff --git a/implementation/src/test/java/io/smallrye/config/LoggingConfigSourceInterceptorTest.java b/implementation/src/test/java/io/smallrye/config/LoggingConfigSourceInterceptorTest.java index f854d8378..a93e49667 100644 --- a/implementation/src/test/java/io/smallrye/config/LoggingConfigSourceInterceptorTest.java +++ b/implementation/src/test/java/io/smallrye/config/LoggingConfigSourceInterceptorTest.java @@ -1,14 +1,16 @@ package io.smallrye.config; import static io.smallrye.config.KeyValuesConfigSource.config; +import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_LOG_VALUES; +import static java.util.logging.Level.ALL; import static java.util.stream.Collectors.toList; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; import java.util.NoSuchElementException; -import java.util.logging.Level; import java.util.logging.LogRecord; import org.eclipse.microprofile.config.Config; @@ -20,21 +22,65 @@ class LoggingConfigSourceInterceptorTest { @RegisterExtension - static LogCapture logCapture = LogCapture.with(logRecord -> logRecord.getMessage().startsWith("SRCFG"), Level.ALL); + static LogCapture logCapture = LogCapture.with( + logRecord -> logRecord.getMessage().startsWith("SRCFG01001") || logRecord.getMessage().startsWith("SRCFG01002"), + ALL); @BeforeEach void setUp() { logCapture.records().clear(); } + @Test + void disabled() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultSources() + .addDefaultInterceptors() + .withSources(config("my.prop", "1234")) + .build(); + + assertEquals("1234", config.getRawValue("my.prop")); + assertTrue(logCapture.records().stream().map(LogRecord::getMessage).findAny().isEmpty()); + } + @Test void interceptor() throws Exception { Config config = new SmallRyeConfigBuilder() .addDefaultSources() .addDefaultInterceptors() + .withDefaultValue(SMALLRYE_CONFIG_LOG_VALUES, "true") .withSources(new ConfigValuePropertiesConfigSource( LoggingConfigSourceInterceptorTest.class.getResource("/config-values.properties"))) + .withSecretKeys("secret") + .build(); + + assertEquals("abc", config.getValue("my.prop", String.class)); + // No log is done here to not expose any sensitive information + assertThrows(SecurityException.class, () -> config.getValue("secret", String.class)); + assertThrows(NoSuchElementException.class, () -> config.getValue("not.found", String.class)); + + // This should not log the secret value: + assertEquals("12345678", SecretKeys.doUnlocked(() -> config.getValue("secret", String.class))); + + List logs = logCapture.records().stream().map(LogRecord::getMessage).collect(toList()); + // my.prop lookup + assertTrue(logs.stream() + .anyMatch(log -> log.contains("The config my.prop was loaded from ConfigValuePropertiesConfigSource"))); + assertTrue(logs.stream().anyMatch(log -> log.contains(":1 with the value abc"))); + // not.found lookup + assertTrue(logs.contains("SRCFG01002: The config not.found was not found")); + // secret lookup, shows the key but hides the source and value + assertTrue(logs.contains("SRCFG01001: The config secret was loaded from secret with the value secret")); + } + + @Test + void explicitInterceptor() throws Exception { + Config config = new SmallRyeConfigBuilder() + .addDefaultSources() + .addDefaultInterceptors() .withInterceptors(new LoggingConfigSourceInterceptor()) + .withSources(new ConfigValuePropertiesConfigSource( + LoggingConfigSourceInterceptorTest.class.getResource("/config-values.properties"))) .withSecretKeys("secret") .build(); @@ -46,37 +92,44 @@ void interceptor() throws Exception { // This should not log the secret value: assertEquals("12345678", SecretKeys.doUnlocked(() -> config.getValue("secret", String.class))); - // 1st element is the SR profile parent lookup - // 2nd element is the MP profile lookup - // 3rd element is the SR profile lookup - // 4th element is the MP Property Expressions List logs = logCapture.records().stream().map(LogRecord::getMessage).collect(toList()); // my.prop lookup - assertTrue(logs.get(4).startsWith("SRCFG01001")); - assertTrue(logs.get(4).contains("The config my.prop was loaded from ConfigValuePropertiesConfigSource")); - assertTrue(logs.get(4).contains(":1 with the value abc")); + assertTrue(logs.stream() + .anyMatch(log -> log.contains("The config my.prop was loaded from ConfigValuePropertiesConfigSource"))); + assertTrue(logs.stream().anyMatch(log -> log.contains(":1 with the value abc"))); // not.found lookup - assertEquals("SRCFG01002: The config not.found was not found", logs.get(5)); + assertTrue(logs.contains("SRCFG01002: The config not.found was not found")); // secret lookup, shows the key but hides the source and value - assertEquals("SRCFG01001: The config secret was loaded from secret with the value secret", logs.get(6)); + assertTrue(logs.contains("SRCFG01001: The config secret was loaded from secret with the value secret")); } @Test void expansion() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .addDefaultInterceptors() - .withInterceptors(new LoggingConfigSourceInterceptor()) + .withDefaultValue(SMALLRYE_CONFIG_LOG_VALUES, "true") .withSources(config("my.prop.expand", "${expand}", "expand", "1234")) .build(); assertEquals("1234", config.getRawValue("my.prop.expand")); - // 1st element is the SR profile parent lookup - // 2nd element is the MP profile lookup - // 3rd element is the SR profile lookup - // 4th element is the MP Property Expressions List logs = logCapture.records().stream().map(LogRecord::getMessage).collect(toList()); - assertEquals("SRCFG01001: The config my.prop.expand was loaded from KeyValuesConfigSource with the value ${expand}", - logs.get(4)); - assertEquals("SRCFG01001: The config expand was loaded from KeyValuesConfigSource with the value 1234", logs.get(5)); + assertTrue(logs.contains( + "SRCFG01001: The config my.prop.expand was loaded from KeyValuesConfigSource with the value ${expand}")); + assertTrue(logs.contains("SRCFG01001: The config expand was loaded from KeyValuesConfigSource with the value 1234")); + } + + @Test + void profiles() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withDefaultValue(SMALLRYE_CONFIG_LOG_VALUES, "true") + .withSources(config("%prod.my.prop", "1234", "my.prop", "5678")) + .withProfile("prod") + .build(); + + assertEquals("1234", config.getRawValue("my.prop")); + List logs = logCapture.records().stream().map(LogRecord::getMessage).collect(toList()); + assertTrue(logs.contains("SRCFG01001: The config my.prop was loaded from KeyValuesConfigSource with the value 1234")); + assertFalse(logs.stream().anyMatch(log -> log.contains("%prod.my.prop"))); } } diff --git a/implementation/src/test/java/io/smallrye/config/MultiValueTest.java b/implementation/src/test/java/io/smallrye/config/MultiValueTest.java index 4fa83c760..e21931e88 100644 --- a/implementation/src/test/java/io/smallrye/config/MultiValueTest.java +++ b/implementation/src/test/java/io/smallrye/config/MultiValueTest.java @@ -24,9 +24,10 @@ void setUp() { Properties properties = new Properties(); properties.put("my.pets", "snake,dog,cat,cat"); - config = (SmallRyeConfig) SmallRyeConfigProviderResolver.instance().getBuilder() + config = SmallRyeConfigProviderResolver.instance().getBuilder() .withSources(new PropertiesConfigSource(properties, "my properties")) - .build(); + .build() + .unwrap(SmallRyeConfig.class); } @Test diff --git a/implementation/src/test/java/io/smallrye/config/NameIteratorTest.java b/implementation/src/test/java/io/smallrye/config/NameIteratorTest.java index a59e96755..eeeabf6c1 100644 --- a/implementation/src/test/java/io/smallrye/config/NameIteratorTest.java +++ b/implementation/src/test/java/io/smallrye/config/NameIteratorTest.java @@ -34,4 +34,14 @@ void previous() { nameIterator.previous(); assertEquals("foo", nameIterator.getNextSegment()); } + + @Test + void quotes() { + NameIterator nameIterator = new NameIterator("one.\"two.three\".four"); + assertEquals("one", nameIterator.getNextSegment()); + nameIterator.next(); + assertEquals("two.three", nameIterator.getNextSegment()); + nameIterator.next(); + assertEquals("four", nameIterator.getNextSegment()); + } } diff --git a/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java b/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java new file mode 100644 index 000000000..9c13c5848 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ObjectCreatorTest.java @@ -0,0 +1,604 @@ +package io.smallrye.config; + +import static io.smallrye.config.ConfigMappings.getNames; +import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; +import static io.smallrye.config.KeyValuesConfigSource.config; +import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +public class ObjectCreatorTest { + @Test + void objectCreator() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "unnamed.value", "unnamed", + "unnamed.key.value", "value")) + .withSources(config( + "list-map[0].key", "value", + "list-map[0].another", "another")) + .withSources(config( + "list-group[0].value", "value")) + .withSources(config( + "map-group.key.value", "value")) + .withSources(config( + "optional-group.value", "value")) + .withSources(config( + "group.value", "value")) + .withSources(config( + "value", "value")) + .withSources(config( + "optional-value", "value")) + .withSources(config( + "optional-list", "value")) + .withSources(config( + "optional-list-group[0].value", "value")) + .build(); + + ConfigMappingContext context = new ConfigMappingContext(config, new HashMap<>(), + getNames(configClassWithPrefix(ObjectCreator.class))); + ObjectCreator mapping = new ObjectCreatorImpl(context); + + assertEquals(2, mapping.unnamed().size()); + assertEquals("unnamed", mapping.unnamed().get("unnamed").value()); + assertEquals("value", mapping.unnamed().get("key").value()); + + assertEquals("value", mapping.listMap().get(0).get("key")); + assertEquals("another", mapping.listMap().get(0).get("another")); + + assertEquals("value", mapping.listGroup().get(0).value()); + + assertEquals("value", mapping.mapGroup().get("key").value()); + + assertTrue(mapping.optionalGroup().isPresent()); + assertEquals("value", mapping.optionalGroup().get().value()); + assertTrue(mapping.optionalGroupMissing().isEmpty()); + assertEquals("value", mapping.group().value()); + + assertEquals("value", mapping.value()); + assertTrue(mapping.optionalValue().isPresent()); + assertEquals("value", mapping.optionalValue().get()); + assertTrue(mapping.optionalList().isPresent()); + assertEquals("value", mapping.optionalList().get().get(0)); + assertTrue(mapping.optionalListGroup().isPresent()); + assertEquals("value", mapping.optionalListGroup().get().get(0).value()); + assertTrue(mapping.optionalListGroupMissing().isEmpty()); + + assertTrue(context.getProblems().isEmpty()); + } + + @ConfigMapping + interface ObjectCreator { + @WithUnnamedKey + Map unnamed(); + + List> listMap(); + + List listGroup(); + + Map mapGroup(); + + Optional optionalGroup(); + + Optional optionalGroupMissing(); + + Nested group(); + + String value(); + + Optional optionalValue(); + + Optional> optionalList(); + + Optional> optionalListGroup(); + + Optional> optionalListGroupMissing(); + + interface Nested { + String value(); + } + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + static class ObjectCreatorImpl implements ObjectCreator { + Map unnamed; + List> listMap; + List listGroup; + Map mapGroup; + Optional optionalGroup; + Optional optionalGroupMissing; + Nested group; + String value; + Optional optionalValue; + Optional> optionalList; + Optional> optionalListGroup; + Optional> optionalListGroupMissing; + + @SuppressWarnings("unchecked") + public ObjectCreatorImpl(ConfigMappingContext context) { + StringBuilder sb = context.getStringBuilder(); + int length = sb.length(); + ConfigMapping.NamingStrategy ns = ConfigMapping.NamingStrategy.KEBAB_CASE; + + sb.append(ns.apply("unnamed")); + ConfigMappingContext.ObjectCreator> unnamed = context.new ObjectCreator>( + sb.toString()) + .map(String.class, null, "unnamed") + .lazyGroup(Nested.class); + this.unnamed = unnamed.get(); + sb.setLength(length); + + sb.append(ns.apply("list-map")); + ConfigMappingContext.ObjectCreator>> listMap = context.new ObjectCreator>>( + sb.toString()).collection(List.class).values(String.class, null, String.class, null, null); + this.listMap = listMap.get(); + sb.setLength(length); + + sb.append(ns.apply("list-group")); + ConfigMappingContext.ObjectCreator> listGroup = context.new ObjectCreator>(sb.toString()) + .collection(List.class).group(Nested.class); + this.listGroup = listGroup.get(); + sb.setLength(length); + + sb.append(ns.apply("map-group")); + ConfigMappingContext.ObjectCreator> mapGroup = context.new ObjectCreator>( + sb.toString()).map(String.class, null).lazyGroup(Nested.class); + this.mapGroup = mapGroup.get(); + sb.setLength(length); + + sb.append(ns.apply("optional-group")); + ConfigMappingContext.ObjectCreator> optionalGroup = context.new ObjectCreator>( + sb.toString()).optionalGroup(Nested.class); + this.optionalGroup = optionalGroup.get(); + sb.setLength(length); + + sb.append(ns.apply("optional-group-missing")); + ConfigMappingContext.ObjectCreator> optionalGroupMissing = context.new ObjectCreator>( + sb.toString()).optionalGroup(Nested.class); + this.optionalGroupMissing = optionalGroupMissing.get(); + sb.setLength(length); + + sb.append(ns.apply("group")); + ConfigMappingContext.ObjectCreator group = context.new ObjectCreator(sb.toString()) + .group(Nested.class); + this.group = group.get(); + sb.setLength(length); + + sb.append(ns.apply("value")); + ConfigMappingContext.ObjectCreator value = context.new ObjectCreator(sb.toString()) + .value(String.class, null); + this.value = value.get(); + sb.setLength(length); + + sb.append(ns.apply("optional-value")); + ConfigMappingContext.ObjectCreator> optionalValue = context.new ObjectCreator>( + sb.toString()).optionalValue(String.class, null); + this.optionalValue = optionalValue.get(); + sb.setLength(length); + + sb.append(ns.apply("optional-value")); + ConfigMappingContext.ObjectCreator>> optionalList = context.new ObjectCreator>>( + sb.toString()).optionalValues(String.class, null, List.class); + this.optionalList = optionalList.get(); + sb.setLength(length); + + sb.append(ns.apply("optional-list-group")); + ConfigMappingContext.ObjectCreator>> optionalListGroup = context.new ObjectCreator>>( + sb.toString()).optionalCollection(List.class).group(Nested.class); + this.optionalListGroup = optionalListGroup.get(); + sb.setLength(length); + + sb.append(ns.apply("optional-list-group-missing")); + ConfigMappingContext.ObjectCreator>> optionalListGroupMissing = context.new ObjectCreator>>( + sb.toString()).optionalCollection(List.class).group(Nested.class); + this.optionalListGroupMissing = optionalListGroupMissing.get(); + sb.setLength(length); + } + + @Override + public Map unnamed() { + return unnamed; + } + + @Override + public List> listMap() { + return listMap; + } + + @Override + public List listGroup() { + return listGroup; + } + + @Override + public Map mapGroup() { + return mapGroup; + } + + @Override + public Optional optionalGroup() { + return optionalGroup; + } + + @Override + public Optional optionalGroupMissing() { + return optionalGroupMissing; + } + + @Override + public Nested group() { + return group; + } + + @Override + public String value() { + return value; + } + + @Override + public Optional optionalValue() { + return optionalValue; + } + + @Override + public Optional> optionalList() { + return optionalList; + } + + @Override + public Optional> optionalListGroup() { + return optionalListGroup; + } + + @Override + public Optional> optionalListGroupMissing() { + return optionalListGroupMissing; + } + } + + @Test + void optionalGroup() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "optional.value", "value")) + .build(); + + ConfigMappingContext context = new ConfigMappingContext(config, new HashMap<>(), + getNames(configClassWithPrefix(OptionalGroup.class))); + OptionalGroup mapping = new OptionalGroupImpl(context); + + assertTrue(mapping.optional().isPresent()); + assertTrue(mapping.empty().isEmpty()); + + assertTrue(context.getProblems().isEmpty()); + } + + @ConfigMapping + interface OptionalGroup { + Optional optional(); + + Optional empty(); + + interface Nested { + String value(); + } + } + + static class OptionalGroupImpl implements OptionalGroup { + Optional optional; + Optional empty; + + public OptionalGroupImpl(ConfigMappingContext context) { + StringBuilder sb = context.getStringBuilder(); + int length = sb.length(); + ConfigMapping.NamingStrategy ns = ConfigMapping.NamingStrategy.KEBAB_CASE; + + sb.append(ns.apply("optional")); + ConfigMappingContext.ObjectCreator> optional = context.new ObjectCreator>( + sb.toString()).optionalGroup(Nested.class); + this.optional = optional.get(); + sb.setLength(length); + + sb.append(ns.apply("empty")); + ConfigMappingContext.ObjectCreator> empty = context.new ObjectCreator>( + sb.toString()).optionalGroup(Nested.class); + this.empty = empty.get(); + sb.setLength(length); + } + + @Override + public Optional optional() { + return optional; + } + + @Override + public Optional empty() { + return empty; + } + } + + @Test + void unnamedKeys() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(UnnamedKeys.class) + .withSources(config( + "unnamed.value", "unnamed", + "unnamed.key.value", "value")) + .build(); + + ConfigMappingContext context = new ConfigMappingContext(config, new HashMap<>(), + getNames(configClassWithPrefix(UnnamedKeys.class))); + context.getStringBuilder().append("unnamed"); + + UnnamedKeys mapping = new UnnamedKeysImpl(context); + assertEquals("unnamed", mapping.map().get(null).value()); + assertEquals("value", mapping.map().get("key").value()); + + mapping = config.getConfigMapping(UnnamedKeys.class); + assertEquals("unnamed", mapping.map().get(null).value()); + assertEquals("value", mapping.map().get("key").value()); + + assertTrue(context.getProblems().isEmpty()); + } + + @ConfigMapping(prefix = "unnamed") + interface UnnamedKeys { + @WithUnnamedKey + @WithParentName + Map map(); + + interface Nested { + String value(); + } + } + + static class UnnamedKeysImpl implements UnnamedKeys { + Map map; + + public UnnamedKeysImpl(ConfigMappingContext context) { + StringBuilder sb = context.getStringBuilder(); + int length = sb.length(); + + ConfigMappingContext.ObjectCreator> map = context.new ObjectCreator>( + sb.toString()) + .map(String.class, null, "") + .lazyGroup(Nested.class); + this.map = map.get(); + sb.setLength(length); + } + + @Override + public Map map() { + return map; + } + } + + @Test + void mapDefaults() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(MapDefaults.class) + .withSources(config( + "map.defaults.one", "value")) + .withSources(config( + "map.defaults-nested.one.value", "value")) + .withSources(config( + "map.defaults-list.one[0].value", "value")) + .build(); + + ConfigMappingContext context = new ConfigMappingContext(config, new HashMap<>(), + getNames(configClassWithPrefix(MapDefaults.class))); + context.getStringBuilder().append("map."); + MapDefaults mapping = new MapDefaultsImpl(context); + + assertEquals("value", mapping.defaults().get("one")); + assertEquals("default", mapping.defaults().get("default")); + assertEquals("default", mapping.defaults().get("something")); + + assertEquals("value", mapping.defaultsNested().get("one").value()); + assertEquals("default", mapping.defaultsNested().get("default").value()); + assertEquals("another", mapping.defaultsNested().get("default").another().another()); + + assertEquals("value", mapping.defaultsList().get("one").get(0).value()); + + assertTrue(context.getProblems().isEmpty()); + } + + @ConfigMapping(prefix = "map") + interface MapDefaults { + @WithDefault("default") + Map defaults(); + + @WithDefaults + Map defaultsNested(); + + @WithDefaults + Map> defaultsList(); + + interface Nested { + @WithDefault("default") + String value(); + + AnotherNested another(); + + interface AnotherNested { + @WithDefault("another") + String another(); + } + } + } + + @SuppressWarnings("unchecked") + static class MapDefaultsImpl implements MapDefaults { + Map defaults; + Map defaultsNested; + Map> defaultsList; + + public MapDefaultsImpl(ConfigMappingContext context) { + StringBuilder sb = context.getStringBuilder(); + int length = sb.length(); + ConfigMapping.NamingStrategy ns = ConfigMapping.NamingStrategy.KEBAB_CASE; + + sb.append(ns.apply("defaults")); + ConfigMappingContext.ObjectCreator> defaults = context.new ObjectCreator>( + sb.toString()) + .values(String.class, null, String.class, null, "default"); + this.defaults = defaults.get(); + sb.setLength(length); + + sb.append(ns.apply("defaults-nested")); + ConfigMappingContext.ObjectCreator> defaultsNested = context.new ObjectCreator>( + sb.toString()) + .map(String.class, null, null, new Supplier() { + @Override + public Nested get() { + sb.append(".*"); + Nested nested = context.constructGroup(Nested.class); + sb.setLength(length); + return nested; + } + }) + .lazyGroup(Nested.class); + this.defaultsNested = defaultsNested.get(); + sb.setLength(length); + + sb.append(ns.apply("defaults-list")); + ConfigMappingContext.ObjectCreator>> defaultsList = context.new ObjectCreator>>( + sb.toString()) + .map(String.class, null) + .collection(List.class) + .lazyGroup(Nested.class); + this.defaultsList = defaultsList.get(); + sb.setLength(length); + } + + @Override + public Map defaults() { + return defaults; + } + + @Override + public Map defaultsNested() { + return defaultsNested; + } + + @Override + public Map> defaultsList() { + return defaultsList; + } + } + + @Test + void namingStrategy() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "naming.nested_value.value", "value")) + .build(); + + ConfigMappingContext context = new ConfigMappingContext(config, new HashMap<>(), + getNames(configClassWithPrefix(Naming.class))); + context.getStringBuilder().append("naming."); + Naming naming = new NamingImpl(context); + + assertEquals("value", naming.nestedValue().value()); + + assertTrue(context.getProblems().isEmpty()); + } + + @ConfigMapping(prefix = "naming", namingStrategy = ConfigMapping.NamingStrategy.SNAKE_CASE) + interface Naming { + Nested nestedValue(); + + interface Nested { + String value(); + } + } + + static class NamingImpl implements Naming { + Nested nestedValue; + + public NamingImpl(ConfigMappingContext context) { + ConfigMapping.NamingStrategy ns = ConfigMapping.NamingStrategy.SNAKE_CASE; + StringBuilder sb = context.getStringBuilder(); + int length = sb.length(); + + sb.append(ns.apply("nestedValue")); + ConfigMappingContext.ObjectCreator nestedValue = context.new ObjectCreator(sb.toString()) + .group(Nested.class); + this.nestedValue = nestedValue.get(); + sb.setLength(length); + } + + @Override + public Nested nestedValue() { + return nestedValue; + } + } + + @Test + void splitRootsRequiredGroup() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config(SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN, "false")) + .withDefaultValue("nested.nested.something", "something") + .withMapping(SplitRootsRequiredGroup.class) + .build(); + + SplitRootsRequiredGroup mapping = config.getConfigMapping(SplitRootsRequiredGroup.class); + assertTrue(mapping.nested().isEmpty()); + } + + @ConfigMapping + interface SplitRootsRequiredGroup { + + Optional nested(); + + interface NestedOptional { + @WithName("x") + Optional nestedOpt(); + } + + interface Nested { + String value(); + } + } + + @Test + void hierarchy() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "base.nested.base", "value", + "base.nested.value", "value")) + .withMapping(ExtendsBase.class) + .build(); + + ExtendsBase mapping = config.getConfigMapping(ExtendsBase.class); + + assertTrue(mapping.nested().isPresent()); + assertEquals("value", mapping.nested().get().base()); + assertEquals("value", mapping.nested().get().value()); + } + + public interface Base { + Optional nested(); + + interface NestedBase { + String base(); + } + + interface Nested extends NestedBase { + String value(); + } + } + + @ConfigMapping(prefix = "base") + public interface ExtendsBase extends Base { + + } +} diff --git a/implementation/src/test/java/io/smallrye/config/ProfileConfigSourceInterceptorTest.java b/implementation/src/test/java/io/smallrye/config/ProfileConfigSourceInterceptorTest.java index c8df842b0..336f6af94 100644 --- a/implementation/src/test/java/io/smallrye/config/ProfileConfigSourceInterceptorTest.java +++ b/implementation/src/test/java/io/smallrye/config/ProfileConfigSourceInterceptorTest.java @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -20,6 +21,7 @@ import java.util.NoSuchElementException; import java.util.OptionalInt; import java.util.Set; +import java.util.stream.StreamSupport; import org.eclipse.microprofile.config.Config; import org.junit.jupiter.api.Test; @@ -29,7 +31,7 @@ class ProfileConfigSourceInterceptorTest { @Test void profile() { - final Config config = buildConfig("my.prop", "1", "%prof.my.prop", "2", SMALLRYE_CONFIG_PROFILE, "prof"); + SmallRyeConfig config = buildConfig("my.prop", "1", "%prof.my.prop", "2", SMALLRYE_CONFIG_PROFILE, "prof"); assertEquals("2", config.getValue("my.prop", String.class)); @@ -40,28 +42,28 @@ void profile() { @Test void profileOnly() { - final Config config = buildConfig("%prof.my.prop", "2", SMALLRYE_CONFIG_PROFILE, "prof"); + SmallRyeConfig config = buildConfig("%prof.my.prop", "2", SMALLRYE_CONFIG_PROFILE, "prof"); assertEquals("2", config.getValue("my.prop", String.class)); } @Test void fallback() { - final Config config = buildConfig("my.prop", "1", SMALLRYE_CONFIG_PROFILE, "prof"); + SmallRyeConfig config = buildConfig("my.prop", "1", SMALLRYE_CONFIG_PROFILE, "prof"); assertEquals("1", config.getValue("my.prop", String.class)); } @Test void expressions() { - final Config config = buildConfig("my.prop", "1", "%prof.my.prop", "${my.prop}", SMALLRYE_CONFIG_PROFILE, "prof"); + SmallRyeConfig config = buildConfig("my.prop", "1", "%prof.my.prop", "${my.prop}", SMALLRYE_CONFIG_PROFILE, "prof"); assertThrows(IllegalArgumentException.class, () -> config.getValue("my.prop", String.class)); } @Test void profileExpressions() { - final Config config = buildConfig("my.prop", "1", + SmallRyeConfig config = buildConfig("my.prop", "1", "%prof.my.prop", "${%prof.my.prop.profile}", "%prof.my.prop.profile", "2", SMALLRYE_CONFIG_PROFILE, "prof"); @@ -71,7 +73,7 @@ void profileExpressions() { @Test void cannotExpand() { - final Config config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .addDefaultInterceptors() .withSources(config("my.prop", "${another.prop}", SMALLRYE_CONFIG_PROFILE, "prof", "config_ordinal", "1000")) .withSources(config("my.prop", "${another.prop}", "%prof.my.prop", "2", SMALLRYE_CONFIG_PROFILE, "prof")) @@ -82,8 +84,8 @@ void cannotExpand() { @Test void customConfigProfile() { - final String[] configs = { "my.prop", "1", "%prof.my.prop", "2", "config.profile", "prof" }; - final Config config = new SmallRyeConfigBuilder() + String[] configs = { "my.prop", "1", "%prof.my.prop", "2", "config.profile", "prof" }; + SmallRyeConfig config = new SmallRyeConfigBuilder() .addDefaultSources() .addDiscoveredInterceptors() .withSources(config(configs)) @@ -94,8 +96,8 @@ void customConfigProfile() { @Test void noConfigProfile() { - final String[] configs = { "my.prop", "1", "%prof.my.prop", "2" }; - final Config config = new SmallRyeConfigBuilder() + String[] configs = { "my.prop", "1", "%prof.my.prop", "2" }; + SmallRyeConfig config = new SmallRyeConfigBuilder() .addDefaultInterceptors() .withSources(config(configs)) .build(); @@ -105,8 +107,9 @@ void noConfigProfile() { @Test void priorityProfile() { - final Config config = new SmallRyeConfigBuilder() - .addDefaultSources() + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withProfile("prof") .withSources( new MapBackedConfigSource("higher", new HashMap() { { @@ -121,9 +124,6 @@ void priorityProfile() { } }, 100) { }) - .withInterceptors( - new ProfileConfigSourceInterceptor("prof"), - new ExpressionConfigSourceInterceptor()) .build(); assertEquals("higher-profile", config.getValue("my.prop", String.class)); @@ -131,8 +131,8 @@ void priorityProfile() { @Test void priorityOverrideProfile() { - final Config config = new SmallRyeConfigBuilder() - .addDefaultSources() + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() .withSources( new MapBackedConfigSource("higher", new HashMap() { { @@ -147,9 +147,6 @@ void priorityOverrideProfile() { } }, 100) { }) - .withInterceptors( - new ProfileConfigSourceInterceptor("prof"), - new ExpressionConfigSourceInterceptor()) .build(); assertEquals("higher", config.getValue("my.prop", String.class)); @@ -157,8 +154,9 @@ void priorityOverrideProfile() { @Test void priorityProfileOverOriginal() { - final Config config = new SmallRyeConfigBuilder() - .addDefaultSources() + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withProfile("prof") .withSources( new MapBackedConfigSource("higher", new HashMap() { { @@ -174,9 +172,6 @@ void priorityProfileOverOriginal() { } }, 100) { }) - .withInterceptors( - new ProfileConfigSourceInterceptor("prof"), - new ExpressionConfigSourceInterceptor()) .build(); assertEquals("higher-profile", config.getValue("my.prop", String.class)); @@ -184,30 +179,36 @@ void priorityProfileOverOriginal() { @Test void propertyNames() { - final Config config = buildConfig("my.prop", "1", "%prof.my.prop", "2", "%prof.prof.only", "1", + SmallRyeConfig config = buildConfig( + "my.prop", "1", + "%prof.my.prop", "2", + "%prof.prof.only", "1", + "%inactive.prop", "1", SMALLRYE_CONFIG_PROFILE, "prof"); assertEquals("2", config.getConfigValue("my.prop").getValue()); assertEquals("1", config.getConfigValue("prof.only").getValue()); - final List properties = stream(config.getPropertyNames().spliterator(), false).collect(toList()); + List properties = stream(config.getPropertyNames().spliterator(), false).collect(toList()); assertFalse(properties.contains("%prof.my.prop")); assertTrue(properties.contains("my.prop")); assertTrue(properties.contains("prof.only")); + // Inactive profile properties are included. We may need to revise this + assertTrue(properties.contains("%inactive.prop")); } @Test void excludePropertiesFromInactiveProfiles() { - final Config config = buildConfig("%prof.my.prop", "1", "%foo.another", "2"); + SmallRyeConfig config = buildConfig("%prof.my.prop", "1", "%foo.another", "2"); - final List properties = stream(config.getPropertyNames().spliterator(), false).collect(toList()); + List properties = stream(config.getPropertyNames().spliterator(), false).collect(toList()); assertTrue(properties.contains("my.prop")); assertFalse(properties.contains("another")); } @Test void profileName() { - final SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfig config = new SmallRyeConfigBuilder() .withSources(config("my.prop", "1", "%prof.my.prop", "2")) .withProfile("prof") .build(); @@ -377,7 +378,7 @@ void parentProfileInActiveProfileWithRelocate() { SmallRyeConfig config = new SmallRyeConfigBuilder() .withInterceptorFactories(new ConfigSourceInterceptorFactory() { @Override - public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context) { + public ConfigSourceInterceptor getInterceptor(ConfigSourceInterceptorContext context) { Map relocations = new HashMap<>(); relocations.put(SMALLRYE_CONFIG_PROFILE_PARENT, "quarkus.config.profile.parent"); @@ -480,12 +481,72 @@ void hierarchicalParentProfileMultiple() { assertArrayEquals(new String[] { "b", "a", "2", "1", "d", "c" }, config.getProfiles().toArray(new String[6])); } + @Test + void multipleProfileProperty() { + SmallRyeConfigBuilder builder = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withSources(config("%prod.my.override", "override", "config_ordinal", "1000")) + .withSources(config("%prod,dev.my.prop", "value", "%prod,dev.my.override", "value", "config_ordinal", "100")) + .withSources(config("%dev.my.prop", "minimal", "config_ordinal", "0")) + .withSources(config("%prod,dev.another.prop", "multi", "%prod.another.prop", "single")) + .withSources(config("%common,prod,dev.triple.prop", "triple", "%common,prod.triple.prop", "double")) + .withSources(config("%commonone,prodone,devone.prop.start.with", "1")); + + SmallRyeConfig prod = builder.withProfile("prod").build(); + assertEquals("value", prod.getRawValue("my.prop")); + assertEquals("value", prod.getRawValue("%prod.my.prop")); + assertEquals("override", prod.getRawValue("my.override")); + assertEquals("override", prod.getRawValue("%prod.my.override")); + assertEquals("single", prod.getRawValue("another.prop")); + assertEquals("double", prod.getRawValue("triple.prop")); + Set prodNames = StreamSupport.stream(prod.getPropertyNames().spliterator(), false).collect(toSet()); + assertTrue(prodNames.contains("my.prop")); + assertTrue(prodNames.contains("my.override")); + assertTrue(prodNames.contains("another.prop")); + assertTrue(prodNames.contains("triple.prop")); + assertFalse(prodNames.contains("prop.start.with")); + builder.getProfiles().clear(); + + SmallRyeConfig dev = builder.withProfile("dev").build(); + assertEquals("value", dev.getRawValue("my.prop")); + assertEquals("value", dev.getRawValue("%dev.my.prop")); + assertEquals("value", dev.getRawValue("my.override")); + assertEquals("value", dev.getRawValue("%dev.my.override")); + assertEquals("triple", dev.getRawValue("triple.prop")); + Set devNames = StreamSupport.stream(dev.getPropertyNames().spliterator(), false).collect(toSet()); + assertTrue(devNames.contains("my.prop")); + assertTrue(devNames.contains("my.override")); + assertTrue(devNames.contains("another.prop")); + assertTrue(devNames.contains("triple.prop")); + assertFalse(devNames.contains("prop.start.with")); + builder.getProfiles().clear(); + + SmallRyeConfig common = builder.withProfile("common").build(); + assertEquals("double", common.getRawValue("triple.prop")); + Set commonNames = StreamSupport.stream(common.getPropertyNames().spliterator(), false).collect(toSet()); + assertTrue(commonNames.contains("triple.prop")); + assertFalse(commonNames.contains("my.prop")); + assertFalse(commonNames.contains("prop.start.with")); + builder.getProfiles().clear(); + } + + @Test + void duplicatedProfilesActive() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withSources(config(SMALLRYE_CONFIG_PROFILE, "prod,kubernetes")) + .withSources(config(SMALLRYE_CONFIG_PROFILE_PARENT, "cluster")) + .withSources(config("%kubernetes." + SMALLRYE_CONFIG_PROFILE_PARENT, "cluster")) + .build(); + + assertIterableEquals(List.of("kubernetes", "prod", "cluster"), config.getProfiles()); + } + private static SmallRyeConfig buildConfig(String... keyValues) { return new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withProfile("prof") .withSources(config(keyValues)) - .withInterceptors( - new ProfileConfigSourceInterceptor("prof"), - new ExpressionConfigSourceInterceptor()) .build(); } } diff --git a/implementation/src/test/java/io/smallrye/config/PropertiesLocationConfigSourceFactoryTest.java b/implementation/src/test/java/io/smallrye/config/PropertiesLocationConfigSourceFactoryTest.java index 92ba9c2f8..7d27ba963 100644 --- a/implementation/src/test/java/io/smallrye/config/PropertiesLocationConfigSourceFactoryTest.java +++ b/implementation/src/test/java/io/smallrye/config/PropertiesLocationConfigSourceFactoryTest.java @@ -2,6 +2,7 @@ import static io.smallrye.config.KeyValuesConfigSource.config; import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_LOCATIONS; +import static java.util.logging.Level.ALL; import static java.util.stream.StreamSupport.stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -17,9 +18,9 @@ import java.nio.file.Paths; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.logging.Level; import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.ConfigSourceProvider; @@ -35,7 +36,7 @@ class PropertiesLocationConfigSourceFactoryTest { @RegisterExtension - static LogCapture logCapture = LogCapture.with(logRecord -> logRecord.getMessage().startsWith("SRCFG"), Level.ALL); + static LogCapture logCapture = LogCapture.with(logRecord -> logRecord.getMessage().startsWith("SRCFG01005"), ALL); @BeforeEach void setUp() { @@ -112,6 +113,13 @@ void notFound() { assertNull(config.getRawValue("my.prop")); assertEquals(0, countSources(config)); + + assertThrows(IllegalArgumentException.class, () -> buildConfig("file:/not/found/not-found.properties"), + "Failed to load resource file:/not/found/not-found.properties"); + assertThrows(IllegalArgumentException.class, () -> buildConfig("http://not.found/not-found-properties"), + "Failed to load resource http://not.found/not-found-properties"); + assertThrows(IllegalArgumentException.class, () -> buildConfig("jar:file:/resources-one.jar!/"), + "Failed to load resource jar:file:/resources-one.jar!/"); } @Test @@ -123,7 +131,7 @@ void noPropertiesFile() { @Test void invalidHttp() { - assertThrows(IllegalStateException.class, + assertThrows(IllegalArgumentException.class, () -> buildConfig("https://raw.githubusercontent.com/smallrye/smallrye-config/notfound.properties")); buildConfig("https://github.com/smallrye/smallrye-config/blob/3cc4809734d7fbd03852a20b5870ca743a2427bc/pom.xml"); } @@ -175,14 +183,13 @@ void onlyProfileFile(@TempDir Path tempDir) throws Exception { devProperties.store(out, null); } - SmallRyeConfig config = new SmallRyeConfigBuilder() + SmallRyeConfigBuilder builder = new SmallRyeConfigBuilder() .addDiscoveredSources() .addDefaultInterceptors() .withProfile("common,dev") - .withDefaultValue(SMALLRYE_CONFIG_LOCATIONS, tempDir.resolve("config.properties").toUri().toString()) - .build(); + .withDefaultValue(SMALLRYE_CONFIG_LOCATIONS, tempDir.resolve("config.properties").toUri().toString()); - assertNull(config.getRawValue("my.prop.profile")); + assertThrows(IllegalArgumentException.class, builder::build); } @Test @@ -345,6 +352,49 @@ public Iterable getConfigSources(final ConfigSourceContext context assertEquals("dev", config.getRawValue("context.dev")); } + @Test + void missingFile() { + assertThrows(IllegalArgumentException.class, () -> buildConfig("file:/not-found.properties")); + } + + @Test + void multipleProfilesAndFiles(@TempDir Path tempDir) throws Exception { + Properties mainProperties = new Properties(); + mainProperties.setProperty("my.prop", "main"); + mainProperties.setProperty("only-in-unprofiled", "unprofiled"); + try (FileOutputStream out = new FileOutputStream(tempDir.resolve("application.properties").toFile())) { + mainProperties.store(out, null); + } + + Properties baseProperties = new Properties(); + baseProperties.setProperty("my.prop", "base"); + baseProperties.setProperty("only-in-base", "base"); + try (FileOutputStream out = new FileOutputStream(tempDir.resolve("application-base.properties").toFile())) { + baseProperties.store(out, null); + } + + Properties prodProperties = new Properties(); + prodProperties.setProperty("my.prop", "prod"); + prodProperties.setProperty("only-in-prod", "prod"); + try (FileOutputStream out = new FileOutputStream(tempDir.resolve("application-prod.properties").toFile())) { + prodProperties.store(out, null); + } + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDiscoveredSources() + .addDefaultInterceptors() + .withDefaultValue(SMALLRYE_CONFIG_LOCATIONS, tempDir.resolve("application.properties").toUri().toString()) + .withProfiles(List.of("base", "prod")) + .build(); + + assertTrue(config.getProfiles().contains("base")); + assertTrue(config.getProfiles().contains("prod")); + assertEquals("unprofiled", config.getRawValue("only-in-unprofiled")); + assertEquals("base", config.getRawValue("only-in-base")); + assertEquals("prod", config.getRawValue("only-in-prod")); + assertEquals("prod", config.getRawValue("my.prop")); + } + private static SmallRyeConfig buildConfig(String... locations) { return new SmallRyeConfigBuilder() .addDiscoveredSources() diff --git a/implementation/src/test/java/io/smallrye/config/PropertyNameTest.java b/implementation/src/test/java/io/smallrye/config/PropertyNameTest.java new file mode 100644 index 000000000..73ad0d8f4 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/PropertyNameTest.java @@ -0,0 +1,52 @@ +package io.smallrye.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.junit.jupiter.api.Test; + +class PropertyNameTest { + @Test + void mappingNameEquals() { + assertEquals(new PropertyName(new String("foo")), new PropertyName(new String("foo"))); + assertEquals(new PropertyName(new String("foo.bar")), new PropertyName(new String("foo.bar"))); + assertEquals(new PropertyName("foo.*"), new PropertyName("foo.bar")); + assertEquals(new PropertyName(new String("foo.*")), new PropertyName(new String("foo.*"))); + assertEquals(new PropertyName("*"), new PropertyName("foo")); + assertEquals(new PropertyName("foo"), new PropertyName("*")); + assertEquals(new PropertyName("foo.*.bar"), new PropertyName("foo.bar.bar")); + assertEquals(new PropertyName("foo.bar.bar"), new PropertyName("foo.*.bar")); + assertEquals(new PropertyName("foo.*.bar"), new PropertyName("foo.\"bar\".bar")); + assertEquals(new PropertyName("foo.\"bar\".bar"), new PropertyName("foo.*.bar")); + assertEquals(new PropertyName("foo.*.bar"), new PropertyName("foo.\"bar-baz\".bar")); + assertEquals(new PropertyName("foo.\"bar-baz\".bar"), new PropertyName("foo.*.bar")); + assertNotEquals(new PropertyName("foo.*.bar"), new PropertyName("foo.bar.baz")); + assertNotEquals(new PropertyName("foo.bar.baz"), new PropertyName("foo.*.bar")); + assertEquals(new PropertyName(new String("foo.bar[*]")), new PropertyName(new String("foo.bar[*]"))); + assertEquals(new PropertyName("foo.bar[*]"), new PropertyName("foo.bar[0]")); + assertEquals(new PropertyName("foo.bar[0]"), new PropertyName("foo.bar[*]")); + assertEquals(new PropertyName("foo.*[*]"), new PropertyName("foo.bar[0]")); + assertEquals(new PropertyName("foo.bar[0]"), new PropertyName("foo.*[*]")); + assertEquals(new PropertyName("foo.*[*]"), new PropertyName("foo.baz[1]")); + assertEquals(new PropertyName("foo.baz[1]"), new PropertyName("foo.*[*]")); + assertNotEquals(new PropertyName("foo.*[*]"), new PropertyName("foo.baz[x]")); + assertNotEquals(new PropertyName("foo.baz[x]"), new PropertyName("foo.*[*]")); + assertEquals(new PropertyName("foo.*[*].bar[*]"), new PropertyName("foo.baz[0].bar[0]")); + assertEquals(new PropertyName(new String("foo.*[*].bar[*]")), new PropertyName(new String("foo.*[*].bar[*]"))); + assertEquals(new PropertyName("foo.baz[0].bar[0]"), new PropertyName("foo.*[*].bar[*]")); + assertEquals(new PropertyName(new String("foo.baz[0].bar[0]")), new PropertyName(new String("foo.baz[0].bar[0]"))); + assertNotEquals(new PropertyName("foo.bar.baz[*]").hashCode(), new PropertyName("foo.bar.*").hashCode()); + assertNotEquals(new PropertyName("foo.bar.baz[*]"), new PropertyName("foo.bar.*")); + + assertEquals(new PropertyName("foo").hashCode(), new PropertyName("foo").hashCode()); + assertEquals(new PropertyName("foo.bar").hashCode(), new PropertyName("foo.bar").hashCode()); + assertEquals(new PropertyName("foo.*").hashCode(), new PropertyName("foo.bar").hashCode()); + assertEquals(new PropertyName("foo.*.bar").hashCode(), new PropertyName("foo.bar.bar").hashCode()); + assertEquals(new PropertyName("foo.*.bar").hashCode(), new PropertyName("foo.\"bar\".bar").hashCode()); + assertEquals(new PropertyName(new String("foo.\"bar\".bar")).hashCode(), + new PropertyName(new String("foo.\"bar\".bar")).hashCode()); + assertEquals(new PropertyName("foo.*.bar").hashCode(), new PropertyName("foo.\"bar-baz\".bar").hashCode()); + assertEquals(new PropertyName(new String("foo.\"bar-baz\".bar")).hashCode(), + new PropertyName(new String("foo.\"bar-baz\".bar")).hashCode()); + } +} diff --git a/implementation/src/test/java/io/smallrye/config/RelocateConfigSourceInterceptorTest.java b/implementation/src/test/java/io/smallrye/config/RelocateConfigSourceInterceptorTest.java index 3dbef177a..e82022543 100644 --- a/implementation/src/test/java/io/smallrye/config/RelocateConfigSourceInterceptorTest.java +++ b/implementation/src/test/java/io/smallrye/config/RelocateConfigSourceInterceptorTest.java @@ -1,5 +1,7 @@ package io.smallrye.config; +import static io.smallrye.config.ConfigMappings.mappedProperties; +import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; import static io.smallrye.config.KeyValuesConfigSource.config; import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_PROFILE; import static java.util.Collections.singletonMap; @@ -15,8 +17,10 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.OptionalInt; import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.microprofile.config.Config; import org.junit.jupiter.api.Test; @@ -57,7 +61,7 @@ void fallbackEmpty() { .withSources(config("mp.jwt.token.header", "")).build(); ConfigValue configValue = (ConfigValue) config.getConfigValue("smallrye.jwt.token.header"); - assertEquals("smallrye.jwt.token.header", configValue.getName()); + assertEquals("mp.jwt.token.header", configValue.getName()); assertEquals("Authorization", configValue.getValue()); } @@ -136,87 +140,26 @@ void relocateIsSecret() { @Test void relocatePropertyNames() { - Config config = buildConfig("smallrye.jwt.token.header", "Authorization"); + SmallRyeConfig config = buildConfig("smallrye.jwt.token.header", "Authorization"); - assertEquals("Authorization", config.getValue("smallrye.jwt.token.header", String.class)); + assertEquals("Authorization", config.getRawValue("smallrye.jwt.token.header")); + assertEquals("Authorization", config.getRawValue("mp.jwt.token.header")); List names = stream(config.getPropertyNames().spliterator(), false).collect(toList()); assertEquals(2, names.size()); assertTrue(names.contains("smallrye.jwt.token.header")); assertTrue(names.contains("mp.jwt.token.header")); - - RelocateConfigSourceInterceptor relocateInterceptor = new RelocateConfigSourceInterceptor( - s -> s.replaceAll("smallrye\\.jwt\\.token\\.header", "mp.jwt.token.header")); - Iterator configValues = relocateInterceptor.iterateValues(new ConfigSourceInterceptorContext() { - @Override - public ConfigValue proceed(final String name) { - return null; - } - - @Override - public Iterator iterateNames() { - return null; - } - - @Override - public Iterator iterateValues() { - Set values = new HashSet<>(); - values.add( - ConfigValue.builder().withName("smallrye.jwt.token.header").withValue("Authorization").build()); - return values.iterator(); - } - }); - - Map values = new HashMap<>(); - while (configValues.hasNext()) { - ConfigValue configValue = configValues.next(); - values.put(configValue.getName(), configValue); - } - - assertEquals(2, values.size()); - assertEquals("Authorization", values.get("smallrye.jwt.token.header").getValue()); - assertEquals("Authorization", values.get("mp.jwt.token.header").getValue()); } @Test void fallbackPropertyNames() { SmallRyeConfig config = buildConfig("mp.jwt.token.header", "Authorization"); - assertEquals("Authorization", config.getValue("smallrye.jwt.token.header", String.class)); + assertEquals("Authorization", config.getRawValue("smallrye.jwt.token.header")); + assertEquals("Authorization", config.getRawValue("mp.jwt.token.header")); List names = stream(config.getPropertyNames().spliterator(), false).collect(toList()); assertEquals(2, names.size()); assertTrue(names.contains("smallrye.jwt.token.header")); assertTrue(names.contains("mp.jwt.token.header")); - - FallbackConfigSourceInterceptor fallbackInterceptor = new FallbackConfigSourceInterceptor( - s -> s.replaceAll("mp\\.jwt\\.token\\.header", "smallrye.jwt.token.header")); - Iterator configValues = fallbackInterceptor.iterateValues(new ConfigSourceInterceptorContext() { - @Override - public ConfigValue proceed(final String name) { - return null; - } - - @Override - public Iterator iterateNames() { - return null; - } - - @Override - public Iterator iterateValues() { - Set values = new HashSet<>(); - values.add(ConfigValue.builder().withName("mp.jwt.token.header").withValue("Authorization").build()); - return values.iterator(); - } - }); - - Map values = new HashMap<>(); - while (configValues.hasNext()) { - ConfigValue configValue = configValues.next(); - values.put(configValue.getName(), configValue); - } - - assertEquals(2, values.size()); - assertEquals("Authorization", values.get("smallrye.jwt.token.header").getValue()); - assertEquals("Authorization", values.get("mp.jwt.token.header").getValue()); } @Test @@ -246,9 +189,207 @@ void fallbackPropertyNameToProfile() { ConfigValue value = config.getConfigValue("new"); assertEquals("5678", value.getValue()); - assertEquals("old", value.getName()); + assertEquals("new", value.getName()); assertEquals("dev", value.getProfile()); - assertEquals("%dev.old", value.getNameProfiled()); + assertEquals("%dev.new", value.getNameProfiled()); + } + + @Test + void fallbackMaps() { + FallbackConfigSourceInterceptor fallbackConfigSourceInterceptor = new FallbackConfigSourceInterceptor(name -> { + if (name.startsWith("child.")) { + return "parent." + name.substring(6); + } + return name; + }) { + @Override + public Iterator iterateNames(final ConfigSourceInterceptorContext context) { + Set names = new HashSet<>(); + Set hierarchyCandidates = new HashSet<>(); + Iterator namesIterator = context.iterateNames(); + while (namesIterator.hasNext()) { + String name = namesIterator.next(); + names.add(name); + if (name.startsWith("parent.")) { + hierarchyCandidates.add("child." + name.substring(7)); + } + } + names.addAll(mappedProperties(configClassWithPrefix(Child.class), hierarchyCandidates)); + return names.iterator(); + } + }; + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "parent.parent", "parent", "child.child", "child", + "parent.value", "parent", + "parent.labels.parent", "parent", + "parent.nested.parent.value", "parent-nested")) + .withInterceptors(fallbackConfigSourceInterceptor) + .withMapping(Parent.class) + .withMapping(Child.class) + .build(); + + Parent parent = config.getConfigMapping(Parent.class); + Child child = config.getConfigMapping(Child.class); + + assertEquals("parent", parent.value().orElse(null)); + assertEquals("parent", child.value().orElse(null)); + assertEquals("parent", parent.labels().get("parent")); + assertEquals("parent", child.labels().get("parent")); + assertEquals("parent-nested", parent.nested().get("parent").value()); + assertEquals("parent-nested", child.nested().get("parent").value()); + + config = new SmallRyeConfigBuilder() + .withSources(config( + "parent.parent", "parent", "child.child", "child", + "parent.value", "parent", "child.value", "child", + "parent.labels.parent", "parent", "child.labels.child", "child", + "parent.nested.parent.value", "parent-nested", "child.nested.child.value", "child-nested")) + .withInterceptors(fallbackConfigSourceInterceptor) + .withMapping(Parent.class) + .withMapping(Child.class) + .build(); + + parent = config.getConfigMapping(Parent.class); + child = config.getConfigMapping(Child.class); + + assertEquals("parent", parent.value().orElse(null)); + assertEquals("child", child.value().orElse(null)); + assertEquals(1, parent.labels().size()); + assertEquals("parent", parent.labels().get("parent")); + assertEquals(2, child.labels().size()); + assertEquals("child", child.labels().get("child")); + assertEquals(1, parent.nested().size()); + assertEquals("parent-nested", parent.nested().get("parent").value()); + assertEquals(2, child.nested().size()); + assertEquals("child-nested", child.nested().get("child").value()); + } + + @ConfigMapping(prefix = "parent") + interface Parent { + String parent(); + + Optional value(); + + Map labels(); + + Map nested(); + + interface Nested { + String value(); + } + } + + @ConfigMapping(prefix = "child") + interface Child { + String child(); + + Optional value(); + + Map labels(); + + Map nested(); + + interface Nested { + String value(); + } + } + + @Test + void relocateMapping() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withInterceptors(new RelocateConfigSourceInterceptor(name -> name.replaceFirst("old", "new"))) + .withSources(config("config_ordinal", "1", + "reloc.old.value", "old", + "reloc.old.list[0]", "old", "reloc.old.list[1]", "old", + "reloc.old.map.key", "old", "reloc.old.map.old", "old")) + .withSources(config("config_ordinal", "2", + "reloc.new.value", "new", + "reloc.new.list[0]", "new", + "reloc.new.map.key", "new", "reloc.old.map.new", "new")) + .withMapping(RelocateMapping.class) + .build(); + + RelocateMapping mapping = config.getConfigMapping(RelocateMapping.class); + + assertEquals("new", mapping.value()); + // TODO - Maps and Lists are merged. Is this what we want? Related with https://github.com/quarkusio/quarkus/issues/38786 + assertEquals(2, mapping.list().size()); + assertEquals("new", mapping.list().get(0)); + assertEquals("old", mapping.list().get(1)); + assertEquals(3, mapping.map().size()); + assertEquals("new", mapping.map().get("key")); + assertEquals("new", mapping.map().get("new")); + assertEquals("old", mapping.map().get("old")); + + Set properties = stream(config.getPropertyNames().spliterator(), false).collect(Collectors.toSet()); + Set mappedProperties = mappedProperties(configClassWithPrefix(RelocateMapping.class), properties); + properties.removeAll(mappedProperties); + Set relocateProperties = new HashSet<>(); + for (String property : properties) { + if (mappedProperties.contains(property)) { + ConfigValue configValue = config.getConfigValue(property); + relocateProperties.add(configValue.getName()); + } + } + properties.removeAll(relocateProperties); + } + + @Test + void fallbackMapping() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withInterceptors(new RelocateConfigSourceInterceptor(name -> name.replaceFirst("old", "new"))) + .withInterceptors(new FallbackConfigSourceInterceptor(name -> name.replaceFirst("new", "old"))) + .withSources(config( + "fall.old.value", "old", + "fall.old.list[0]", "old", "fall.old.list[1]", "old", + "fall.old.map.key", "old", "fall.old.map.old", "old")) + .withMapping(FallbackMapping.class) + .build(); + + FallbackMapping mapping = config.getConfigMapping(FallbackMapping.class); + + assertEquals("old", mapping.value()); + assertEquals(2, mapping.list().size()); + assertEquals("old", mapping.list().get(0)); + assertEquals("old", mapping.list().get(1)); + assertEquals(2, mapping.map().size()); + assertEquals("old", mapping.map().get("key")); + assertEquals("old", mapping.map().get("old")); + + Set properties = stream(config.getPropertyNames().spliterator(), false).collect(Collectors.toSet()); + Set mappedProperties = mappedProperties(configClassWithPrefix(FallbackMapping.class), properties); + properties.removeAll(mappedProperties); + Set fallbackProperties = new HashSet<>(); + for (String property : properties) { + ConfigValue configValue = config.getConfigValue(property); + if (mappedProperties.contains(configValue.getName())) { + fallbackProperties.add(property); + } + } + properties.removeAll(fallbackProperties); + assertTrue(properties.isEmpty()); + } + + @ConfigMapping(prefix = "reloc.old") + interface RelocateMapping { + String value(); + + List list(); + + Map map(); + } + + @ConfigMapping(prefix = "fall.new") + interface FallbackMapping { + String value(); + + List list(); + + Map map(); } private static SmallRyeConfig buildConfig(String... keyValues) { diff --git a/implementation/src/test/java/io/smallrye/config/SecretKeysHandlerTest.java b/implementation/src/test/java/io/smallrye/config/SecretKeysHandlerTest.java new file mode 100644 index 000000000..146398eb3 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/SecretKeysHandlerTest.java @@ -0,0 +1,74 @@ +package io.smallrye.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.NoSuchElementException; + +import org.junit.jupiter.api.Test; + +class SecretKeysHandlerTest { + @Test + void notFound() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .addDiscoveredSecretKeysHandlers() + .withDefaultValue("my.secret", "${missing::secret}") + .build(); + + assertThrows(NoSuchElementException.class, () -> config.getConfigValue("my.secret")); + } + + @Test + void handler() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withSecretKeysHandlers(new SecretKeysHandler() { + @Override + public String decode(final String secret) { + return "decoded"; + } + + @Override + public String getName() { + return "handler"; + } + }) + .withDefaultValue("my.secret", "${handler::secret}") + .build(); + + assertEquals("decoded", config.getRawValue("my.secret")); + } + + @Test + void handlerFactory() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withDefaultValue("context.handler", "decoded") + .addDefaultInterceptors() + .withSecretKeyHandlerFactories(new SecretKeysHandlerFactory() { + @Override + public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context) { + return new SecretKeysHandler() { + @Override + public String decode(final String secret) { + return context.getValue("context.handler").getValue(); + } + + @Override + public String getName() { + return "handler"; + } + }; + } + + @Override + public String getName() { + return "handler"; + } + }) + .withDefaultValue("my.secret", "${handler::secret}") + .build(); + + assertEquals("decoded", config.getRawValue("my.secret")); + } +} diff --git a/implementation/src/test/java/io/smallrye/config/SecretKeysTest.java b/implementation/src/test/java/io/smallrye/config/SecretKeysTest.java index a279c469a..f192fbeb0 100644 --- a/implementation/src/test/java/io/smallrye/config/SecretKeysTest.java +++ b/implementation/src/test/java/io/smallrye/config/SecretKeysTest.java @@ -92,7 +92,6 @@ void mapping() { private static Config buildConfig(String... keyValues) { return new SmallRyeConfigBuilder() - .addDefaultSources() .addDefaultInterceptors() .withSources(KeyValuesConfigSource.config(keyValues)) .withSecretKeys("secret") diff --git a/implementation/src/test/java/io/smallrye/config/SmallRyeConfigTest.java b/implementation/src/test/java/io/smallrye/config/SmallRyeConfigTest.java index 1e7862c78..b0e505a33 100644 --- a/implementation/src/test/java/io/smallrye/config/SmallRyeConfigTest.java +++ b/implementation/src/test/java/io/smallrye/config/SmallRyeConfigTest.java @@ -2,6 +2,7 @@ import static io.smallrye.config.KeyValuesConfigSource.config; import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; import static java.util.stream.Collectors.toSet; import static java.util.stream.StreamSupport.stream; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -14,13 +15,17 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; +import java.util.TreeMap; +import java.util.function.IntFunction; import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.Converter; import org.junit.jupiter.api.Test; import io.smallrye.config.common.AbstractConfigSource; @@ -39,7 +44,8 @@ void getValues() { void getValuesConverter() { SmallRyeConfig config = new SmallRyeConfigBuilder().withSources(config("my.list", "1,2,3,4")).build(); - List values = config.getValues("my.list", config.getConverter(Integer.class).get(), ArrayList::new); + List values = config.getValues("my.list", config.getConverter(Integer.class).get(), + (IntFunction>) value -> new ArrayList<>()); assertEquals(Arrays.asList(1, 2, 3, 4), values); } @@ -57,19 +63,11 @@ void getOptionalValuesConverter() { SmallRyeConfig config = new SmallRyeConfigBuilder().withSources(config("my.list", "1,2,3,4")).build(); Optional> values = config.getOptionalValues("my.list", config.getConverter(Integer.class).get(), - ArrayList::new); + (IntFunction>) value -> new ArrayList<>()); assertTrue(values.isPresent()); assertEquals(Arrays.asList(1, 2, 3, 4), values.get()); } - @Test - void rawValueEquals() { - SmallRyeConfig config = new SmallRyeConfigBuilder().withSources(config("my.prop", "1234")).build(); - - assertTrue(config.rawValueEquals("my.prop", "1234")); - assertFalse(config.rawValueEquals("my.prop", "0")); - } - @Test void convert() { SmallRyeConfig config = new SmallRyeConfigBuilder().build(); @@ -234,6 +232,17 @@ void nestedIndexes() { assertTrue(indexes.contains(1)); } + @Test + void quotedIndexes() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("map.roles.\"quoted.key\"[0].name", "")) + .build(); + + List indexes = config.getIndexedPropertiesIndexes("map.roles.\"quoted.key\""); + assertEquals(1, indexes.size()); + assertTrue(indexes.contains(0)); + } + @Test void overrideIndexedValues() { SmallRyeConfig config = new SmallRyeConfigBuilder() @@ -258,12 +267,8 @@ void overrideIndexedValues() { @Test void isPropertyPresent() { SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(config("my.prop", "1234", "my.expansion", "${not.available}")) - .withSources(new MapBackedConfigSource("hidder", new HashMap() { - { - put("my.hidden", "hidden"); - } - }) { + .withSources(config("my.prop", "1234", "my.expansion", "${not.available}", "empty", "")) + .withSources(new MapBackedConfigSource("hidder", Map.of("my.hidden", "hidden")) { @Override public Set getPropertyNames() { return Collections.emptySet(); @@ -275,9 +280,10 @@ public Set getPropertyNames() { assertTrue(config.isPropertyPresent("my.expansion")); assertFalse(config.isPropertyPresent("not.available")); assertTrue(config.isPropertyPresent("my.hidden")); + assertFalse(config.isPropertyPresent("empty")); Set names = stream(config.getPropertyNames().spliterator(), false).collect(toSet()); - assertEquals(2, names.size()); + assertEquals(3, names.size()); assertTrue(names.contains("my.prop")); assertTrue(names.contains("my.expansion")); } @@ -294,11 +300,9 @@ void getPropertyNames() { config = new SmallRyeConfigBuilder().addDefaultInterceptors().addDefaultSources() .withSources(KeyValuesConfigSource.config("smallrye.mp-config.prop", "5678")).build(); assertEquals("1234", config.getRawValue("SMALLRYE_MP_CONFIG_PROP")); - assertEquals("1234", config.getRawValue("smallrye.mp.config.prop")); assertEquals("1234", config.getRawValue("smallrye.mp-config.prop")); assertTrue(((Set) config.getPropertyNames()).contains("SMALLRYE_MP_CONFIG_PROP")); assertTrue(((Set) config.getPropertyNames()).contains("smallrye.mp-config.prop")); - assertFalse(((Set) config.getPropertyNames()).contains("smallrye.mp.config.prop")); } @Test @@ -347,4 +351,183 @@ void builderWithFlagSetters() { assertTrue(config.getConfigSource("EnvConfigSource").isPresent()); assertEquals("1234", config.getRawValue("my.prop")); } + + @Test + void getValuesMap() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "my.prop.key", "value", + "my.prop.key.nested", "value", + "my.prop.\"key.quoted\"", "value", + "my.prop.key.indexed[0]", "value")) + .build(); + + Map map = config.getValues("my.prop", String.class, String.class); + assertEquals(4, map.size()); + assertEquals("value", map.get("key")); + assertEquals("value", map.get("key.nested")); + assertEquals("value", map.get("key.quoted")); + assertEquals("value", map.get("key.indexed[0]")); + + Converter stringConverter = config.requireConverter(String.class); + Map treeMap = config.getValues("my.prop", stringConverter, stringConverter, t -> new TreeMap<>()); + assertTrue(treeMap instanceof TreeMap); + + Optional> optionalMap = config.getOptionalValues("my.prop", String.class, String.class); + assertTrue(optionalMap.isPresent()); + assertEquals(4, optionalMap.get().size()); + assertEquals("value", optionalMap.get().get("key")); + assertEquals("value", optionalMap.get().get("key.nested")); + assertEquals("value", optionalMap.get().get("key.quoted")); + assertEquals("value", optionalMap.get().get("key.indexed[0]")); + + Optional> optionalTreeMap = config.getOptionalValues("my.prop", stringConverter, stringConverter, + t -> new TreeMap<>()); + assertTrue(optionalTreeMap.isPresent()); + assertTrue(optionalTreeMap.get() instanceof TreeMap); + + assertTrue(config.getOptionalValues("my.optional", String.class, String.class).isEmpty()); + } + + @Test + void getValuesMapInline() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("my.prop", "key=value;key.nested=value;\"key.quoted\"=value")) + .build(); + + Map map = config.getValues("my.prop", String.class, String.class); + assertEquals(3, map.size()); + assertEquals("value", map.get("key")); + assertEquals("value", map.get("key.nested")); + assertEquals("value", map.get("key.quoted")); + + Optional> optionalMap = config.getOptionalValues("my.prop", String.class, String.class); + assertTrue(optionalMap.isPresent()); + assertEquals(3, optionalMap.get().size()); + assertEquals("value", optionalMap.get().get("key")); + assertEquals("value", optionalMap.get().get("key.nested")); + assertEquals("value", optionalMap.get().get("key.quoted")); + } + + @Test + void getValuesMapList() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "my.prop.key[0]", "value", + "my.prop.key[1]", "value", + "my.prop.key.nested[0]", "value", + "my.prop.key.nested[1]", "value", + "my.prop.\"key.quoted\"[0]", "value", + "my.prop.\"key.quoted\"[1]", "value")) + .build(); + + Map> map = config.getValues("my.prop", String.class, String.class, ArrayList::new); + assertEquals(3, map.size()); + assertEquals("value", map.get("key").get(0)); + assertEquals("value", map.get("key").get(1)); + assertEquals("value", map.get("key.nested").get(0)); + assertEquals("value", map.get("key.nested").get(1)); + assertEquals("value", map.get("key.quoted").get(0)); + assertEquals("value", map.get("key.quoted").get(1)); + + Converter stringConverter = config.requireConverter(String.class); + Map> treeMap = config.getValues("my.prop", stringConverter, stringConverter, t -> new TreeMap<>(), + ArrayList::new); + assertTrue(treeMap instanceof TreeMap); + + Optional>> optionalMap = config.getOptionalValues("my.prop", String.class, String.class, + ArrayList::new); + assertTrue(optionalMap.isPresent()); + assertEquals(3, optionalMap.get().size()); + assertEquals("value", optionalMap.get().get("key").get(0)); + assertEquals("value", optionalMap.get().get("key").get(1)); + assertEquals("value", optionalMap.get().get("key.nested").get(0)); + assertEquals("value", optionalMap.get().get("key.nested").get(1)); + assertEquals("value", optionalMap.get().get("key.quoted").get(0)); + assertEquals("value", optionalMap.get().get("key.quoted").get(1)); + + Optional>> optionalTreeMap = config.getOptionalValues("my.prop", stringConverter, + stringConverter, t -> new TreeMap<>(), ArrayList::new); + assertTrue(optionalTreeMap.isPresent()); + assertTrue(optionalTreeMap.get() instanceof TreeMap); + + assertTrue(config.getOptionalValues("my.optional", String.class, String.class, ArrayList::new).isEmpty()); + } + + @Test + void getValuesMapListInline() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config("my.prop", "key=value,value;key.nested=value,value;\"key.quoted\"=value,value")) + .build(); + + Map> map = config.getValues("my.prop", String.class, String.class, ArrayList::new); + assertEquals(3, map.size()); + assertEquals("value", map.get("key").get(0)); + assertEquals("value", map.get("key").get(1)); + assertEquals("value", map.get("key.nested").get(0)); + assertEquals("value", map.get("key.nested").get(1)); + assertEquals("value", map.get("key.quoted").get(0)); + assertEquals("value", map.get("key.quoted").get(1)); + + Optional>> optionalMap = config.getOptionalValues("my.prop", String.class, String.class, + ArrayList::new); + assertTrue(optionalMap.isPresent()); + assertEquals(3, optionalMap.get().size()); + assertEquals("value", optionalMap.get().get("key").get(0)); + assertEquals("value", optionalMap.get().get("key").get(1)); + assertEquals("value", optionalMap.get().get("key.nested").get(0)); + assertEquals("value", optionalMap.get().get("key.nested").get(1)); + assertEquals("value", optionalMap.get().get("key.quoted").get(0)); + assertEquals("value", optionalMap.get().get("key.quoted").get(1)); + } + + @Test + void getValuesMapIntegers() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "my", "nothing", + "my.prop", "nothing", + "my.prop.1", "1", + "my.prop.2", "2", + "my.prop.3", "3")) + .build(); + + Map map = config.getValues("my.prop", Integer.class, Integer.class); + assertEquals(3, map.size()); + assertEquals(1, map.get(1)); + assertEquals(2, map.get(2)); + assertEquals(3, map.get(3)); + } + + @Test + void getValuesMapEmpty() { + SmallRyeConfig config = new SmallRyeConfigBuilder().build(); + assertThrows(NoSuchElementException.class, () -> config.getValues("my.prop", String.class, String.class)); + assertThrows(NoSuchElementException.class, + () -> config.getValues("my.prop", String.class, String.class, ArrayList::new)); + } + + @Test + void quotedKeysInEnv() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withSources(new EnvConfigSource(singletonMap("ENV__QUOTED_KEY__VALUE", "env"), 300)) + .withSources(config("env.\"quoted-key\".value", "default")) + .build(); + + assertEquals("env", config.getRawValue("env.\"quoted-key\".value")); + + ConfigSource keymap = config.getConfigSource("KeyValuesConfigSource").get(); + assertEquals("default", keymap.getValue("env.\"quoted-key\".value")); + } + + @Test + void emptyPropertyNames() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withSources(new EnvConfigSource(singletonMap("", "value"), 300)) + .build(); + + assertEquals("value", config.getRawValue("")); + } } diff --git a/pom.xml b/pom.xml index dc845c2da..e4bf0dcdf 100644 --- a/pom.xml +++ b/pom.xml @@ -22,23 +22,23 @@ io.smallrye smallrye-parent - 35 + 42 io.smallrye.config smallrye-config-parent - 2.11.1 + 3.7.1 pom - SmallRye: MicroProfile Config Parent - http://smallrye.io + SmallRye Config: Parent + https://smallrye.io - 2.0.1 - 1.13.0 - 9.3 + 3.1 + 2.2.0 + 9.6 - 1.0.0 + 2.3.0 SmallRye Config @@ -56,11 +56,11 @@ https://github.com/smallrye/smallrye-config/issues - + scm:git:git@github.com:smallrye/smallrye-config.git scm:git:git@github.com:smallrye/smallrye-config.git https://github.com/smallrye/smallrye-config/ - 2.11.1 + 3.7.1 @@ -72,9 +72,12 @@ sources/file-system sources/yaml sources/zookeeper + sources/keystore converters/json utils/events utils/cdi-provider + utils/crypto + utils/jasypt testsuite documentation examples @@ -105,9 +108,10 @@ io.smallrye.testing - smallrye-testing-utilities + smallrye-testing-bom ${version.smallrye.testing} - test + import + pom diff --git a/release/pom.xml b/release/pom.xml index 89a78e8a3..032191038 100644 --- a/release/pom.xml +++ b/release/pom.xml @@ -5,7 +5,7 @@ io.smallrye.config smallrye-config-parent - 2.11.1 + 3.7.1 smallrye-config-release diff --git a/sources/file-system/pom.xml b/sources/file-system/pom.xml index 4198b61d6..0ab743f86 100644 --- a/sources/file-system/pom.xml +++ b/sources/file-system/pom.xml @@ -4,12 +4,12 @@ smallrye-config-parent io.smallrye.config - 2.11.1 + 3.7.1 ../../pom.xml smallrye-config-source-file-system - SmallRye: FileSystem ConfigSource + SmallRye Config: ConfigSource - FileSystem diff --git a/sources/file-system/src/test/java/io/smallrye/config/source/file/FileSystemConfigSourceFactoryTest.java b/sources/file-system/src/test/java/io/smallrye/config/source/file/FileSystemConfigSourceFactoryTest.java index 946172232..6f3a595bd 100644 --- a/sources/file-system/src/test/java/io/smallrye/config/source/file/FileSystemConfigSourceFactoryTest.java +++ b/sources/file-system/src/test/java/io/smallrye/config/source/file/FileSystemConfigSourceFactoryTest.java @@ -5,7 +5,6 @@ import java.net.URISyntaxException; import java.net.URL; import java.util.Iterator; -import java.util.List; import java.util.stream.StreamSupport; import org.eclipse.microprofile.config.spi.ConfigSource; @@ -40,7 +39,6 @@ void testMultipleLocations() throws URISyntaxException { private ConfigSourceContext newConfigSourceContext(String value) { return new ConfigSourceContext() { - @Override public Iterator iterateNames() { return null; @@ -50,11 +48,6 @@ public Iterator iterateNames() { public ConfigValue getValue(String name) { return new ConfigValueBuilder().withValue(value).build(); } - - @Override - public List getProfiles() { - return null; - } }; } } diff --git a/sources/file-system/src/test/java/io/smallrye/config/source/file/FileSystemConfigSourceTest.java b/sources/file-system/src/test/java/io/smallrye/config/source/file/FileSystemConfigSourceTest.java index 3525fd7cc..e2736f02f 100644 --- a/sources/file-system/src/test/java/io/smallrye/config/source/file/FileSystemConfigSourceTest.java +++ b/sources/file-system/src/test/java/io/smallrye/config/source/file/FileSystemConfigSourceTest.java @@ -48,13 +48,13 @@ void testCharacterReplacement() throws URISyntaxException { File dir = new File(configDirURL.toURI()); ConfigSource configSource = new FileSystemConfigSource(dir); - // the non-alphanumeric chars may be replaced by _ + // the non-alphanumeric chars may be replaced by _ assertEquals("http://localhost:8080/my-service", configSource.getValue("MyService/mp-rest/url")); // or the file name is uppercased assertEquals("http://localhost:8080/other-service", configSource.getValue("OtherService/mp-rest/url")); - // but the key is still case sensitive + // but the key is still case sensitive assertNull(configSource.getValue("myservice/mp-rest/url")); - // you can't rewrite the key, only the file name + // you can't rewrite the key, only the file name assertNull(configSource.getValue("MYSERVICE_MP_REST_URL")); } } diff --git a/sources/hocon/pom.xml b/sources/hocon/pom.xml index 62b2ddd58..80ecd09ba 100644 --- a/sources/hocon/pom.xml +++ b/sources/hocon/pom.xml @@ -4,15 +4,15 @@ smallrye-config-parent io.smallrye.config - 2.11.1 + 3.7.1 ../../pom.xml smallrye-config-source-hocon - SmallRye: HOCON ConfigSource + SmallRye Config: ConfigSource - HOCON - 1.4.2 + 1.4.3 diff --git a/sources/hocon/src/test/java/io/smallrye/config/source/hocon/HoconConfigSourceTest.java b/sources/hocon/src/test/java/io/smallrye/config/source/hocon/HoconConfigSourceTest.java index d1ae5cc7c..68b2e685c 100644 --- a/sources/hocon/src/test/java/io/smallrye/config/source/hocon/HoconConfigSourceTest.java +++ b/sources/hocon/src/test/java/io/smallrye/config/source/hocon/HoconConfigSourceTest.java @@ -4,6 +4,7 @@ import static java.util.Collections.singletonMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Iterator; @@ -98,6 +99,11 @@ void systemFile() { assertEquals("FJ", config.getRawValue("countries[0].code")); } + @Test + void missingFile() { + assertThrows(IllegalArgumentException.class, () -> buildConfig("file:/not-found.conf")); + } + private static SmallRyeConfig buildConfig(String... locations) { return new SmallRyeConfigBuilder() .addDiscoveredSources() diff --git a/sources/keystore/pom.xml b/sources/keystore/pom.xml new file mode 100644 index 000000000..f295e83f5 --- /dev/null +++ b/sources/keystore/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + smallrye-config-parent + io.smallrye.config + 3.7.1 + ../../pom.xml + + + smallrye-config-source-keystore + + SmallRye: MicroProfile Config Source - KeyStore + + + + io.smallrye.config + smallrye-config + + + + + org.junit.jupiter + junit-jupiter + + + io.smallrye.testing + smallrye-testing-utilities + + + jakarta.annotation + jakarta.annotation-api + test + + + + + + + io.smallrye + jandex-maven-plugin + ${version.org.jboss.jandex} + + + make-index + + jandex + + + + + + + + diff --git a/sources/keystore/src/main/java/io/smallrye/config/source/keystore/KeyStoreConfig.java b/sources/keystore/src/main/java/io/smallrye/config/source/keystore/KeyStoreConfig.java new file mode 100644 index 000000000..84b5f5a17 --- /dev/null +++ b/sources/keystore/src/main/java/io/smallrye/config/source/keystore/KeyStoreConfig.java @@ -0,0 +1,31 @@ +package io.smallrye.config.source.keystore; + +import java.util.Map; +import java.util.Optional; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithParentName; + +@ConfigMapping(prefix = "smallrye.config.source.keystore") +public interface KeyStoreConfig { + @WithParentName + Map keystores(); + + interface KeyStore { + String path(); + + @WithDefault("PKCS12") + String type(); + + Optional handler(); + + Map aliases(); + + interface Alias { + Optional name(); + + Optional handler(); + } + } +} diff --git a/sources/keystore/src/main/java/io/smallrye/config/source/keystore/KeyStoreConfigSourceFactory.java b/sources/keystore/src/main/java/io/smallrye/config/source/keystore/KeyStoreConfigSourceFactory.java new file mode 100644 index 000000000..6dbd65a58 --- /dev/null +++ b/sources/keystore/src/main/java/io/smallrye/config/source/keystore/KeyStoreConfigSourceFactory.java @@ -0,0 +1,201 @@ +package io.smallrye.config.source.keystore; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.net.URL; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.config.AbstractLocationConfigSourceFactory; +import io.smallrye.config.ConfigSourceContext; +import io.smallrye.config.ConfigSourceFactory; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config._private.ConfigMessages; +import io.smallrye.config.source.keystore.KeyStoreConfig.KeyStore.Alias; + +public class KeyStoreConfigSourceFactory implements ConfigSourceFactory { + @Override + public Iterable getConfigSources(final ConfigSourceContext context) { + KeyStoreConfig keyStoreConfig = getKeyStoreConfig(context); + + // A keystore may contain the encryption key for a handler, so we load keystore that do not have handlers + Map prioritized = new HashMap<>(); + Map late = new HashMap<>(); + for (Map.Entry keyStoreEntry : keyStoreConfig.keystores().entrySet()) { + if (keyStoreEntry.getValue().handler().isEmpty()) { + prioritized.put(keyStoreEntry.getKey(), keyStoreEntry.getValue()); + } else { + late.put(keyStoreEntry.getKey(), keyStoreEntry.getValue()); + } + } + + List keyStoreSources = new ArrayList<>(); + for (Map.Entry keyStoreEntry : prioritized.entrySet()) { + for (ConfigSource configSource : loadKeyStoreSources(context, keyStoreEntry.getKey(), + keyStoreEntry.getValue())) { + keyStoreSources.add(configSource); + } + } + + ConfigSourceContext keyStoreContext = new ConfigSourceContext() { + final SmallRyeConfig contextConfig = new SmallRyeConfigBuilder() + .withSources(new ConfigSourceContext.ConfigSourceContextConfigSource(context)) + .withSources(keyStoreSources).build(); + + @Override + public ConfigValue getValue(final String name) { + return contextConfig.getConfigValue(name); + } + + @Override + public Iterator iterateNames() { + return contextConfig.getPropertyNames().iterator(); + } + }; + + for (Map.Entry keyStoreEntry : late.entrySet()) { + for (ConfigSource configSource : loadKeyStoreSources(keyStoreContext, keyStoreEntry.getKey(), + keyStoreEntry.getValue())) { + keyStoreSources.add(configSource); + } + } + + return keyStoreSources; + } + + private static KeyStoreConfig getKeyStoreConfig(final ConfigSourceContext context) { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(new ConfigSourceContext.ConfigSourceContextConfigSource(context)) + .withMapping(KeyStoreConfig.class) + .withMappingIgnore("smallrye.config.source.keystore.*.password") + .build(); + return config.getConfigMapping(KeyStoreConfig.class); + } + + private static Iterable loadKeyStoreSources(final ConfigSourceContext context, final String name, + final KeyStoreConfig.KeyStore keyStore) { + return new AbstractLocationConfigSourceFactory() { + @Override + protected String[] getFileExtensions() { + return new String[0]; + } + + @Override + protected ConfigSource loadConfigSource(final URL url, final int ordinal) throws IOException { + ConfigValue password = getPassword(context, name); + return new UrlKeyStoreConfigSource(url, ordinal).loadKeyStore(keyStore, password.getValue().toCharArray()); + } + + @Override + public Iterable getConfigSources(final ConfigSourceContext context) { + return loadConfigSources(keyStore.path(), 100); + } + + }.getConfigSources(context); + } + + // Avoid caching the keystore password + private static ConfigValue getPassword(final ConfigSourceContext context, final String name) { + // TODO - name can be quoted. try to figure out a better way to do this + String passwordName = "smallrye.config.source.keystore." + name + ".password"; + ConfigValue password = context.getValue(passwordName); + if (password == null || password.getValue() == null) { + passwordName = "smallrye.config.source.keystore.\"" + name + "\".password"; + password = context.getValue(passwordName); + if (password == null || password.getValue() == null) { + throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(passwordName)); + } + } + return password; + } + + private static class UrlKeyStoreConfigSource implements ConfigSource { + private final URL url; + private final int ordinal; + + UrlKeyStoreConfigSource(final URL url, final int ordinal) { + this.url = url; + this.ordinal = ordinal; + } + + ConfigSource loadKeyStore(KeyStoreConfig.KeyStore keyStoreConfig, char[] password) throws IOException { + try { + KeyStore keyStore = KeyStore.getInstance(keyStoreConfig.type()); + keyStore.load(url.openStream(), password); + + Map properties = new HashMap<>(); + Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + Alias aliasConfig = keyStoreConfig.aliases().getOrDefault(alias, new Alias() { + @Override + public Optional name() { + return Optional.of(alias); + } + + @Override + public Optional handler() { + return keyStoreConfig.handler(); + } + }); + + if (keyStore.isKeyEntry(alias)) { + Key key = keyStore.getKey(alias, password); + String encoded; + Optional handler = aliasConfig.handler(); + if (handler.isPresent()) { + encoded = "${" + handler.get() + "::" + new String(key.getEncoded(), UTF_8) + "}"; + } else { + encoded = new String(key.getEncoded(), UTF_8); + } + properties.put(aliasConfig.name().orElse(alias), encoded); + } else if (keyStore.isCertificateEntry(alias)) { + // TODO + } + } + return new PropertiesConfigSource(properties, this.getName(), this.getOrdinal()); + } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | UnrecoverableKeyException e) { + throw new RuntimeException(e); + } + } + + @Override + public int getOrdinal() { + return ordinal; + } + + @Override + public Set getPropertyNames() { + throw new UnsupportedOperationException(); + } + + @Override + public String getValue(final String propertyName) { + throw new UnsupportedOperationException(); + } + + @Override + public String getName() { + return "KeyStoreConfigSource[source=" + url.toString() + "]"; + } + } +} diff --git a/sources/keystore/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory b/sources/keystore/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory new file mode 100644 index 000000000..be558b2b6 --- /dev/null +++ b/sources/keystore/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory @@ -0,0 +1 @@ +io.smallrye.config.source.keystore.KeyStoreConfigSourceFactory diff --git a/sources/keystore/src/test/java/io/smallrye/config/source/keystore/KeyStoreConfigSourceTest.java b/sources/keystore/src/test/java/io/smallrye/config/source/keystore/KeyStoreConfigSourceTest.java new file mode 100644 index 000000000..8af2fd79f --- /dev/null +++ b/sources/keystore/src/test/java/io/smallrye/config/source/keystore/KeyStoreConfigSourceTest.java @@ -0,0 +1,56 @@ +package io.smallrye.config.source.keystore; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.smallrye.config.ConfigValue; +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; + +class KeyStoreConfigSourceTest { + @Test + void keystore() { + // keytool -importpass -alias my.secret -keystore keystore -storepass secret -storetype PKCS12 -v + Map properties = Map.of( + "smallrye.config.source.keystore.test.path", "keystore", + "smallrye.config.source.keystore.test.password", "secret"); + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withProfile("prod") + .addDefaultInterceptors() + .addDiscoveredSources() + .withSources(new PropertiesConfigSource(properties, "", 0)) + .build(); + + ConfigValue secret = config.getConfigValue("my.secret"); + assertEquals("secret", secret.getValue()); + } + + @Test + void keyStoreNotFound() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withProfile("prod") + .addDefaultInterceptors() + .addDiscoveredSources() + .withSources(new PropertiesConfigSource(Map.of( + "smallrye.config.source.keystore.test.path", "not.found", + "smallrye.config.source.keystore.test.password", "secret"), "", 0)) + .build(); + + ConfigValue secret = config.getConfigValue("my.secret"); + assertNull(secret.getValue()); + + assertThrows(IllegalArgumentException.class, () -> new SmallRyeConfigBuilder() + .addDiscoveredSources() + .withSources(new PropertiesConfigSource(Map.of( + "smallrye.config.source.keystore.test.path", "file:/not.found", + "smallrye.config.source.keystore.test.password", "secret"), "", 0)) + .build()); + } +} diff --git a/sources/keystore/src/test/resources/keystore b/sources/keystore/src/test/resources/keystore new file mode 100644 index 000000000..10082892f Binary files /dev/null and b/sources/keystore/src/test/resources/keystore differ diff --git a/sources/yaml/pom.xml b/sources/yaml/pom.xml index aedd9fb17..992707dff 100644 --- a/sources/yaml/pom.xml +++ b/sources/yaml/pom.xml @@ -4,16 +4,16 @@ smallrye-config-parent io.smallrye.config - 2.11.1 + 3.7.1 ../../pom.xml smallrye-config-source-yaml - SmallRye: MicroProfile Config Source - Yaml + SmallRye Config: ConfigSource - YAML - 1.30 + 2.2 diff --git a/sources/yaml/src/main/java/io/smallrye/config/source/yaml/YamlConfigSource.java b/sources/yaml/src/main/java/io/smallrye/config/source/yaml/YamlConfigSource.java index cdacb0859..19400f14b 100644 --- a/sources/yaml/src/main/java/io/smallrye/config/source/yaml/YamlConfigSource.java +++ b/sources/yaml/src/main/java/io/smallrye/config/source/yaml/YamlConfigSource.java @@ -8,7 +8,6 @@ import java.net.URL; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -19,8 +18,9 @@ import org.eclipse.microprofile.config.spi.ConfigSource; import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.nodes.Tag; import io.smallrye.common.classloader.ClassPathUtils; @@ -54,11 +54,6 @@ public YamlConfigSource(String name, Map source, int ordinal) { this.propertyNames = filterPropertyNames(source); } - @Deprecated - public YamlConfigSource(String name, InputStream stream) throws IOException { - this(name, stream, ORDINAL); - } - public YamlConfigSource(URL url) throws IOException { this(url, ORDINAL); } @@ -74,11 +69,6 @@ public YamlConfigSource(URL url, int ordinal) throws IOException { }), ordinal); } - @Deprecated - public YamlConfigSource(String name, InputStream stream, int defaultOrdinal) throws IOException { - this(name, streamToMap(stream), defaultOrdinal); - } - public YamlConfigSource(String name, String source) { this(name, source, ORDINAL); } @@ -97,7 +87,7 @@ private static Map streamToMap(InputStream inputStream) throws I Assert.checkNotNullParam("inputStream", inputStream); final Map yamlInput = new TreeMap<>(); try { - final Iterable objects = new Yaml(new StringConstructor()).loadAll(inputStream); + final Iterable objects = new Yaml(new StringConstructor(new LoaderOptions())).loadAll(inputStream); for (Object object : objects) { if (object instanceof Map) { yamlInput.putAll(yamlInputToMap((Map) object)); @@ -117,8 +107,14 @@ private static Map streamToMap(InputStream inputStream) throws I @SuppressWarnings("unchecked") private static Map stringToMap(String str) { - final Map yamlInput = new Yaml(new StringConstructor()).loadAs(str, HashMap.class); - return yamlInputToMap(yamlInput); + final Map yamlInput = new TreeMap<>(); + final Iterable objects = new Yaml(new StringConstructor(new LoaderOptions())).loadAll(str); + for (Object object : objects) { + if (object instanceof Map) { + yamlInput.putAll(yamlInputToMap((Map) object)); + } + } + return yamlInput; } private static Map yamlInputToMap(final Map yamlInput) { @@ -221,12 +217,13 @@ private static Set filterPropertyNames(Map source) { } /** - * Override some of the yaml constructors, so that the value written in the flatten result is more alike with the + * Override some yaml constructors, so that the value written in the flatten result is more alike with the * source. For instance, timestamps may be written in a completely different format which prevents converters to * convert the correct value. */ - private static class StringConstructor extends Constructor { - public StringConstructor() { + private static class StringConstructor extends SafeConstructor { + public StringConstructor(final LoaderOptions loadingConfig) { + super(loadingConfig); this.yamlConstructors.put(Tag.INT, new ConstructYamlStr()); this.yamlConstructors.put(Tag.FLOAT, new ConstructYamlStr()); this.yamlConstructors.put(Tag.TIMESTAMP, new ConstructYamlStr()); diff --git a/sources/yaml/src/test/java/io/smallrye/config/source/yaml/BasicTest.java b/sources/yaml/src/test/java/io/smallrye/config/source/yaml/BasicTest.java index eff36b20c..c9caebcf5 100644 --- a/sources/yaml/src/test/java/io/smallrye/config/source/yaml/BasicTest.java +++ b/sources/yaml/src/test/java/io/smallrye/config/source/yaml/BasicTest.java @@ -46,15 +46,13 @@ void listValue() { @Test void emptyFile() { - String yaml = ""; - ConfigSource src = new YamlConfigSource("Yaml", yaml); + ConfigSource src = new YamlConfigSource("Yaml", ""); assertNotNull(src, "Should create config source for empty file correctly"); } @Test void preserveOriginal() { - String yaml = "date: 2010-10-10"; - ConfigSource source = new YamlConfigSource("Yaml", yaml); + ConfigSource source = new YamlConfigSource("Yaml", "date: 2010-10-10"); assertEquals("2010-10-10", source.getValue("date")); } } diff --git a/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlConfigMappingTest.java b/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlConfigMappingTest.java index b6b141e63..7864af115 100644 --- a/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlConfigMappingTest.java +++ b/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlConfigMappingTest.java @@ -1,29 +1,258 @@ package io.smallrye.config.source.yaml; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalInt; +import org.eclipse.microprofile.config.spi.Converter; import org.junit.jupiter.api.Test; import io.smallrye.config.ConfigMapping; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.WithConverter; import io.smallrye.config.WithDefault; import io.smallrye.config.WithName; import io.smallrye.config.WithParentName; +import io.smallrye.config.WithUnnamedKey; class YamlConfigMappingTest { @Test - void yamlConfigMapping() throws Exception { + void yamlConfigMapping() { + String yaml = "proxies:\n" + + " - type: mssql\n" + + " name: first\n" + + " input:\n" + + " pool:\n" + + " max_pool_size: 100\n" + + " expires_in_seconds: 60\n" + + " mssql:\n" + + " server: 192.168.1.2\n" + + " port: 1434\n" + + " user: 'test'\n" + + " password: 'test'\n" + + " read:\n" + + " pool:\n" + + " max_pool_size: 100\n" + + " expires_in_seconds: 60\n" + + " regex: '(?i)^\\s*select.*'\n" + + " mssql:\n" + + " server: 127.0.0.1\n" + + " port: 1433\n" + + " user: 'sa'\n" + + " password: 'Test!1234'\n" + + " write:\n" + + " pool:\n" + + " max_pool_size: 100\n" + + " expires_in_seconds: 60\n" + + " mssql:\n" + + " server: 127.0.0.1\n" + + " port: 1433\n" + + " user: 'sa'\n" + + " password: 'Test!1234'\n" + + " - type: mssql\n" + + " name: second\n" + + " input:\n" + + " mssql:\n" + + " server: 192.168.1.2\n" + + " port: 1435\n" + + " user: 'test'\n" + + " password: 'test'\n" + + " pool:\n" + + " max_pool_size: 0\n" + + " expires_in_seconds: 0\n" + + " encryption:\n" + + " level: 'SUPPORTED'\n" + + " keystore:\n" + + " location: 'my-location'\n" + + " read:\n" + + " regex: '(?i)^\\s*select.*'\n" + + " mssql:\n" + + " server: 127.0.0.1\n" + + " port: 1433\n" + + " user: 'sa'\n" + + " password: 'Test!1234'\n" + + " write:\n" + + " mssql:\n" + + " server: 127.0.0.1\n" + + " port: 1433\n" + + " user: 'sa'\n" + + " password: 'Test!1234'\n"; + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(Proxies.class, "proxies") - .withSources(new YamlConfigSource(YamlConfigSourceTest.class.getResource("/example-mapping.yml"))) + .withSources(new YamlConfigSource("yaml", yaml)) + .build(); + + Proxies proxies = config.getConfigMapping(Proxies.class); + + assertFalse(proxies.allProxies().isEmpty()); + assertEquals(SQLProxyConfig.DatabaseType.mssql, proxies.allProxies().get(0).type()); + assertEquals("mssql", proxies.allProxies().get(0).typeAsString()); + assertEquals("first", proxies.allProxies().get(0).name()); + assertEquals(-1, proxies.allProxies().get(0).maxAsyncThreads()); + assertEquals(-1, proxies.allProxies().get(0).maxQueuedThreads()); + + assertTrue(proxies.allProxies().get(0).input().pool().isPresent()); + assertEquals(60, proxies.allProxies().get(0).input().pool().get().expiresInSeconds()); + assertTrue(proxies.allProxies().get(0).input().pool().get().maxPoolSize().isPresent()); + assertEquals(100, proxies.allProxies().get(0).input().pool().get().maxPoolSize().getAsInt()); + assertTrue(proxies.allProxies().get(0).input().mssql().isPresent()); + assertEquals("192.168.1.2", proxies.allProxies().get(0).input().mssql().get().server()); + assertEquals(1434, proxies.allProxies().get(0).input().mssql().get().port()); + assertEquals("test", proxies.allProxies().get(0).input().mssql().get().user()); + assertEquals("test", proxies.allProxies().get(0).input().mssql().get().password()); + assertFalse(proxies.allProxies().get(0).input().mssql().get().hostName().isPresent()); + assertFalse(proxies.allProxies().get(0).input().mssql().get().database().isPresent()); + assertFalse(proxies.allProxies().get(0).input().mssql().get().timeout().isPresent()); + assertFalse(proxies.allProxies().get(0).input().regex().isPresent()); + + assertTrue(proxies.allProxies().get(0).read().pool().isPresent()); + assertEquals(60, proxies.allProxies().get(0).read().pool().get().expiresInSeconds()); + assertTrue(proxies.allProxies().get(0).read().pool().get().maxPoolSize().isPresent()); + assertEquals(100, proxies.allProxies().get(0).read().pool().get().maxPoolSize().getAsInt()); + assertTrue(proxies.allProxies().get(0).read().mssql().isPresent()); + assertEquals("127.0.0.1", proxies.allProxies().get(0).read().mssql().get().server()); + assertEquals(1433, proxies.allProxies().get(0).read().mssql().get().port()); + assertEquals("sa", proxies.allProxies().get(0).read().mssql().get().user()); + assertEquals("Test!1234", proxies.allProxies().get(0).read().mssql().get().password()); + assertFalse(proxies.allProxies().get(0).read().mssql().get().hostName().isPresent()); + assertFalse(proxies.allProxies().get(0).read().mssql().get().database().isPresent()); + assertFalse(proxies.allProxies().get(0).read().mssql().get().timeout().isPresent()); + assertTrue(proxies.allProxies().get(0).read().regex().isPresent()); + assertEquals("(?i)^\\s*select.*", proxies.allProxies().get(0).read().regex().get()); + + assertTrue(proxies.allProxies().get(0).write().pool().isPresent()); + assertEquals(60, proxies.allProxies().get(0).write().pool().get().expiresInSeconds()); + assertTrue(proxies.allProxies().get(0).write().pool().get().maxPoolSize().isPresent()); + assertEquals(100, proxies.allProxies().get(0).write().pool().get().maxPoolSize().getAsInt()); + assertTrue(proxies.allProxies().get(0).write().mssql().isPresent()); + assertEquals("127.0.0.1", proxies.allProxies().get(0).write().mssql().get().server()); + assertEquals(1433, proxies.allProxies().get(0).write().mssql().get().port()); + assertEquals("sa", proxies.allProxies().get(0).write().mssql().get().user()); + assertEquals("Test!1234", proxies.allProxies().get(0).write().mssql().get().password()); + assertFalse(proxies.allProxies().get(0).write().mssql().get().hostName().isPresent()); + assertFalse(proxies.allProxies().get(0).write().mssql().get().database().isPresent()); + assertFalse(proxies.allProxies().get(0).write().mssql().get().timeout().isPresent()); + assertFalse(proxies.allProxies().get(0).write().regex().isPresent()); + + assertEquals(SQLProxyConfig.DatabaseType.mssql, proxies.allProxies().get(1).type()); + assertEquals("mssql", proxies.allProxies().get(1).typeAsString()); + assertEquals("second", proxies.allProxies().get(1).name()); + assertEquals(-1, proxies.allProxies().get(1).maxAsyncThreads()); + assertEquals(-1, proxies.allProxies().get(1).maxQueuedThreads()); + + assertTrue(proxies.allProxies().get(1).input().pool().isPresent()); + assertEquals(0, proxies.allProxies().get(1).input().pool().get().expiresInSeconds()); + assertTrue(proxies.allProxies().get(1).input().pool().get().maxPoolSize().isPresent()); + assertEquals(0, proxies.allProxies().get(1).input().pool().get().maxPoolSize().getAsInt()); + + assertTrue(proxies.allProxies().get(1).input().encryption().isPresent()); + assertEquals("SUPPORTED", proxies.allProxies().get(1).input().encryption().get().levelAsString()); + assertEquals("TLS", proxies.allProxies().get(1).input().encryption().get().sslProtocol()); + assertTrue(proxies.allProxies().get(1).input().encryption().get().keystore().isPresent()); + assertEquals("my-location", proxies.allProxies().get(1).input().encryption().get().keystore().get().location()); + assertEquals("PCKS12", proxies.allProxies().get(1).input().encryption().get().keystore().get().format()); + } + + @Test + void jsonConfigMapping() { + String json = "{\n" + + " \"proxies\": [\n" + + " {\n" + + " \"type\": \"mssql\",\n" + + " \"name\": \"first\",\n" + + " \"input\": {\n" + + " \"pool\": {\n" + + " \"max_pool_size\": 100,\n" + + " \"expires_in_seconds\": 60\n" + + " },\n" + + " \"mssql\": {\n" + + " \"server\": \"192.168.1.2\",\n" + + " \"port\": 1434,\n" + + " \"user\": \"test\",\n" + + " \"password\": \"test\"\n" + + " }\n" + + " },\n" + + " \"read\": {\n" + + " \"pool\": {\n" + + " \"max_pool_size\": 100,\n" + + " \"expires_in_seconds\": 60\n" + + " },\n" + + " \"regex\": \"(?i)^\\\\s*select.*\",\n" + + " \"mssql\": {\n" + + " \"server\": \"127.0.0.1\",\n" + + " \"port\": 1433,\n" + + " \"user\": \"sa\",\n" + + " \"password\": \"Test!1234\"\n" + + " }\n" + + " },\n" + + " \"write\": {\n" + + " \"pool\": {\n" + + " \"max_pool_size\": 100,\n" + + " \"expires_in_seconds\": 60\n" + + " },\n" + + " \"mssql\": {\n" + + " \"server\": \"127.0.0.1\",\n" + + " \"port\": 1433,\n" + + " \"user\": \"sa\",\n" + + " \"password\": \"Test!1234\"\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"type\": \"mssql\",\n" + + " \"name\": \"second\",\n" + + " \"input\": {\n" + + " \"mssql\": {\n" + + " \"server\": \"192.168.1.2\",\n" + + " \"port\": 1435,\n" + + " \"user\": \"test\",\n" + + " \"password\": \"test\"\n" + + " },\n" + + " \"pool\": {\n" + + " \"max_pool_size\": 0,\n" + + " \"expires_in_seconds\": 0\n" + + " },\n" + + " \"encryption\": {\n" + + " \"level\": \"SUPPORTED\",\n" + + " \"keystore\": {\n" + + " \"location\": \"my-location\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"read\": {\n" + + " \"regex\": \"(?i)^\\\\s*select.*\",\n" + + " \"mssql\": {\n" + + " \"server\": \"127.0.0.1\",\n" + + " \"port\": 1433,\n" + + " \"user\": \"sa\",\n" + + " \"password\": \"Test!1234\"\n" + + " }\n" + + " },\n" + + " \"write\": {\n" + + " \"mssql\": {\n" + + " \"server\": \"127.0.0.1\",\n" + + " \"port\": 1433,\n" + + " \"user\": \"sa\",\n" + + " \"password\": \"Test!1234\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(Proxies.class, "proxies") + .withSources(new YamlConfigSource("json", json)) .build(); Proxies proxies = config.getConfigMapping(Proxies.class); @@ -238,17 +467,18 @@ interface KeystoreConfig { @Test void yamlListMaps() { + String yaml = "app:\n" + + " config:\n" + + " - name: Bob\n" + + " foo: thing\n" + + " bar: false\n" + + " - name: Tim\n" + + " baz: stuff\n" + + " qux: 3"; + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(MapConfig.class, "app") - .withSources(new YamlConfigSource("yaml", - "app:\n" + - " config:\n" + - " - name: Bob\n" + - " foo: thing\n" + - " bar: false\n" + - " - name: Tim\n" + - " baz: stuff\n" + - " qux: 3")) + .withSources(new YamlConfigSource("yaml", yaml)) .build(); MapConfig mapping = config.getConfigMapping(MapConfig.class); @@ -267,26 +497,27 @@ interface MapConfig { @Test void yamlMapGroupMap() { + String yaml = "parent:\n" + + " goodchildren:\n" + + " child1:\n" + + " name: John\n" + + " attributes:\n" + + " somekey: somevalue\n" + + " anotherkey: anothervalue\n" + + " child2:\n" + + " name: James\n" + + " attributes:\n" + + " something: isbroken\n" + + " badchildren:\n" + + " child3:\n" + + " name: BadJohn\n" + + " attributes:\n" + + " somekeybad: somevaluebad\n" + + " anotherkeybad: anothervaluebad"; + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(Parent.class, "parent") - .withSources(new YamlConfigSource("yaml", - "parent:\n" + - " goodchildren:\n" + - " child1:\n" + - " name: John\n" + - " attributes:\n" + - " somekey: somevalue\n" + - " anotherkey: anothervalue\n" + - " child2:\n" + - " name: James\n" + - " attributes:\n" + - " something: isbroken\n" + - " badchildren:\n" + - " child3:\n" + - " name: BadJohn\n" + - " attributes:\n" + - " somekeybad: somevaluebad\n" + - " anotherkeybad: anothervaluebad ")) + .withSources(new YamlConfigSource("yaml", yaml)) .build(); Parent mapping = config.getConfigMapping(Parent.class); @@ -324,15 +555,16 @@ interface GrandChild { } @Test - void yamlMapCollections() throws Exception { + void yamlMapCollections() { + String yaml = "some:\n" + + " prop:\n" + + " value: \n" + + " - 0.9\n" + + " - 0.99"; + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(MapListDouble.class) - .withSources(new YamlConfigSource("yaml", - "some:\n" + - " prop:\n" + - " value: \n" + - " - 0.9\n" + - " - 0.99")) + .withSources(new YamlConfigSource("yaml", yaml)) .build(); MapListDouble mapping = config.getConfigMapping(MapListDouble.class); @@ -347,22 +579,23 @@ public interface MapListDouble { @Test void yamlMapListsGroup() { + String yaml = "channelsuite:\n" + + " permissions:\n" + + " anonymous:\n" + + " - \"p1\"\n" + + " internal-call:\n" + + " - \"p2\"\n" + + " - \"p3\"\n" + + " roles:\n" + + " user:\n" + + " - \"p1\"\n" + + " administrator:\n" + + " - \"p2\"\n" + + " - \"p3\"\n"; + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(PermissionsConfig.class) - .withSources(new YamlConfigSource("yaml", - "channelsuite:\n" + - " permissions:\n" + - " anonymous:\n" + - " - \"p1\"\n" + - " internal-call:\n" + - " - \"p2\"\n" + - " - \"p3\"\n" + - " roles:\n" + - " user:\n" + - " - \"p1\"\n" + - " administrator:\n" + - " - \"p2\"\n" + - " - \"p3\"\n")) + .withSources(new YamlConfigSource("yaml", yaml)) .build(); PermissionsConfig configMapping = config.getConfigMapping(PermissionsConfig.class); @@ -385,10 +618,51 @@ interface PermissionsConfig { } @Test - void yamlMapLists() throws Exception { + void yamlMapLists() { + String yaml = "map:\n" + + " roles:\n" + + " hokage:\n" + + " -\n" + + " name: Senju Hashirama\n" + + " nature: Earth,Water\n" + + " -\n" + + " name: Senju Tobirama\n" + + " nature: Water\n" + + " -\n" + + " name: Sarotobi Hiruzen\n" + + " nature: Fire\n" + + " -\n" + + " name: Namikaze Minato\n" + + " nature: Fire,Wind,Lightning\n" + + " -\n" + + " name: Tsunade\n" + + " nature: Lightning\n" + + " -\n" + + " name: Hatake Kakashi\n" + + " nature: Lightning\n" + + " -\n" + + " name: Uzumaki Naruto\n" + + " nature: Wind\n" + + " jutsus:\n" + + " -\n" + + " name: Rasengan\n" + + " rank: A\n" + + " -\n" + + " name: Bijūdama\n" + + " rank: S\n" + + " teams:\n" + + " \"Team Kakashi\":\n" + + " - Hatake Kakashi\n" + + " - Uchiha Sasuke\n" + + " - Haruno Sakura\n" + + " \"Kazekage Rescue Team\":\n" + + " - Hatake Kakashi\n" + + " - Chiyo\n" + + " - Haruno Sakura\n"; + SmallRyeConfig config = new SmallRyeConfigBuilder() .withMapping(MapWithListsGroup.class) - .withSources(new YamlConfigSource(YamlConfigSourceTest.class.getResource("/example-map-list.yml"))) + .withSources(new YamlConfigSource("yaml", yaml)) .build(); MapWithListsGroup mapping = config.getConfigMapping(MapWithListsGroup.class); @@ -439,4 +713,346 @@ enum Rank { } } } + + @Test + void yamlNestedMaps() { + String yaml = "eventStaff:\n" + + " phones:\n" + + " home: \"1\"\n" + + " coordinators:\n" + + " tim:\n" + + " contactInfo:\n" + + " address: \"a\"\n" + + " phones:\n" + + " home: \"1\"\n" + + " cell: \"2\"\n"; + + YamlConfigSource yamlConfigSource = new YamlConfigSource("Yaml", yaml); + + assertEquals("a", yamlConfigSource.getValue("eventStaff.coordinators.tim.contactInfo.address")); + assertEquals("1", yamlConfigSource.getValue("eventStaff.coordinators.tim.contactInfo.phones.home")); + assertEquals("2", yamlConfigSource.getValue("eventStaff.coordinators.tim.contactInfo.phones.cell")); + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withMapping(EventStaff.class, "eventStaff") + .withSources(yamlConfigSource) + .build(); + + EventStaff eventStaff = config.getConfigMapping(EventStaff.class); + + assertEquals("1", eventStaff.phones().get("home")); + assertEquals("a", eventStaff.coordinators().get("tim").contactInfo().address()); + assertEquals("1", eventStaff.coordinators().get("tim").contactInfo().phones().get("home")); + assertEquals("2", eventStaff.coordinators().get("tim").contactInfo().phones().get("cell")); + } + + @ConfigMapping(prefix = "eventStaff", namingStrategy = ConfigMapping.NamingStrategy.VERBATIM) + public interface EventStaff { + Map phones(); + + Map coordinators(); + + interface Coordinator { + ContactInfo contactInfo(); + + interface ContactInfo { + String address(); + + Map phones(); + } + } + } + + @Test + void unnamedMapKeys() { + String yaml = "unnamed:\n" + + " map: \n" + + " value: value\n"; + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .withMapping(UnnamedMapKeys.class) + .withSources(new YamlConfigSource("yaml", yaml)) + .build(); + + UnnamedMapKeys mapping = config.getConfigMapping(UnnamedMapKeys.class); + + assertEquals("value", mapping.map().get(null).value()); + } + + @ConfigMapping(prefix = "unnamed") + interface UnnamedMapKeys { + @WithUnnamedKey + Map map(); + + interface Nested { + String value(); + } + } + + @Test + void unmapped() { + String yaml = "http:\n" + + " server:\n" + + " name: server\n" + + " alias: server\n" + + " host: localhost\n" + + " port: 8080\n" + + " timeout: 60s\n" + + " io-threads: 200\n" + + " bytes: dummy\n" + + "\n" + + " form:\n" + + " login-page: login.html\n" + + " error-page: error.html\n" + + " landing-page: index.html\n" + + " positions:\n" + + " - 10\n" + + " - 20\n" + + "\n" + + " ssl:\n" + + " port: 8443\n" + + " certificate: certificate\n" + + "\n" + + " cors:\n" + + " origins:\n" + + " - host: some-server\n" + + " port: 9000\n" + + " - host: another-server\n" + + " port: 8000\n" + + " methods:\n" + + " - GET\n" + + " - POST\n" + + "\n" + + " log:\n" + + " period: P1D\n" + + " days: 10\n" + + "\n" + + "cloud:\n" + + " host: localhost\n" + + " port: 8080\n" + + " timeout: 60s\n" + + " io-threads: 200\n" + + "\n" + + " form:\n" + + " login-page: login.html\n" + + " error-page: error.html\n" + + " landing-page: index.html\n" + + "\n" + + " ssl:\n" + + " port: 8443\n" + + " certificate: certificate\n" + + "\n" + + " cors:\n" + + " origins:\n" + + " - host: some-server\n" + + " port: 9000\n" + + " - host: localhost\n" + + " port: 1\n" + + " methods:\n" + + " - GET\n" + + " - POST\n" + + "\n" + + " proxy:\n" + + " enable: true\n" + + " timeout: 20\n" + + "\n" + + " log:\n" + + " period: P1D\n" + + " days: 20\n" + + "\n" + + " info:\n" + + " name: Bond\n" + + " code: 007\n" + + " alias:\n" + + " - James\n" + + " admins:\n" + + " root:\n" + + " -\n" + + " username: root\n" + + " -\n" + + " username: super\n" + + " firewall:\n" + + " accepted:\n" + + " - 127.0.0.1\n" + + " - 8.8.8"; + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(new YamlConfigSource("yaml", yaml)) + .withMapping(Server.class) + .withMapping(Cloud.class) + .build(); + + Server server = config.getConfigMapping(Server.class); + assertTrue(server.name().isPresent()); + assertEquals("server", server.name().get()); + assertTrue(server.alias().isPresent()); + assertEquals("server", server.alias().get()); + assertEquals("localhost", server.host()); + assertEquals(8080, server.port()); + assertEquals("60s", server.timeout()); + assertEquals(200, server.threads()); + assertArrayEquals(new Server.ByteArrayConverter().convert("dummy"), server.bytes()); + assertEquals("login.html", server.form().get("form").loginPage()); + assertEquals("error.html", server.form().get("form").errorPage()); + assertEquals("index.html", server.form().get("form").landingPage()); + assertIterableEquals(List.of(10, 20), server.form().get("form").positions()); + assertTrue(server.ssl().isPresent()); + assertEquals(8443, server.ssl().get().port()); + assertEquals("certificate", server.ssl().get().certificate()); + assertIterableEquals(List.of("TLSv1.3", "TLSv1.2"), server.ssl().get().protocols()); + assertTrue(server.cors().isPresent()); + assertEquals("some-server", server.cors().get().origins().get(0).host()); + assertEquals(9000, server.cors().get().origins().get(0).port()); + assertEquals("another-server", server.cors().get().origins().get(1).host()); + assertEquals(8000, server.cors().get().origins().get(1).port()); + assertEquals("GET", server.cors().get().methods().get(0)); + assertEquals("POST", server.cors().get().methods().get(1)); + assertFalse(server.log().enabled()); + assertEquals(".log", server.log().suffix()); + assertTrue(server.log().rotate()); + assertEquals("P1D", server.log().period()); + assertEquals(10, server.log().days()); + assertEquals(Server.Log.Pattern.COMMON.name(), server.log().pattern()); + assertTrue(server.info().name().isEmpty()); + assertTrue(server.info().code().isEmpty()); + assertTrue(server.info().alias().isEmpty()); + assertTrue(server.info().admins().isEmpty()); + assertTrue(server.info().firewall().isEmpty()); + + Cloud cloud = config.getConfigMapping(Cloud.class); + assertTrue(cloud.server().ssl().isPresent()); + assertEquals(8443, cloud.server().ssl().get().port()); + assertEquals("certificate", cloud.server().ssl().get().certificate()); + } + + @ConfigMapping(prefix = "cloud") + public interface Cloud { + @WithParentName + Server server(); + } + + public interface Named { + Optional name(); + } + + public interface Alias extends Named { + Optional alias(); + } + + @ConfigMapping(prefix = "http.server") + public interface Server extends Alias { + String host(); + + int port(); + + String timeout(); + + @WithName("io-threads") + int threads(); + + @WithConverter(ByteArrayConverter.class) + byte[] bytes(); + + @WithParentName + Map form(); + + Optional ssl(); + + Optional proxy(); + + Optional cors(); + + Log log(); + + Info info(); + + interface Form { + String loginPage(); + + String errorPage(); + + String landingPage(); + + Optional cookie(); + + @WithDefault("1") + List positions(); + } + + interface Proxy { + boolean enable(); + + int timeout(); + } + + interface Log { + @WithDefault("false") + boolean enabled(); + + @WithDefault(".log") + String suffix(); + + @WithDefault("true") + boolean rotate(); + + @WithDefault("COMMON") + String pattern(); + + String period(); + + int days(); + + enum Pattern { + COMMON, + SHORT, + COMBINED, + LONG; + } + } + + interface Cors { + List origins(); + + List methods(); + + interface Origin { + String host(); + + int port(); + } + } + + interface Info { + Optional name(); + + OptionalInt code(); + + Optional> alias(); + + Map> admins(); + + Map> firewall(); + + interface Admin { + String username(); + } + } + + class ByteArrayConverter implements Converter { + @Override + public byte[] convert(String value) throws IllegalArgumentException, NullPointerException { + return value.getBytes(StandardCharsets.UTF_8); + } + } + } + + public interface Ssl { + int port(); + + String certificate(); + + @WithDefault("TLSv1.3,TLSv1.2") + List protocols(); + } } diff --git a/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlConfigSourceTest.java b/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlConfigSourceTest.java index 71910281b..7eccf8fe5 100644 --- a/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlConfigSourceTest.java +++ b/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlConfigSourceTest.java @@ -27,24 +27,54 @@ class YamlConfigSourceTest { @Test - void flatten() throws Exception { - YamlConfigSource yaml = new YamlConfigSource(YamlConfigSourceTest.class.getResource("/example-216.yml")); - String value = yaml.getValue("admin.users"); + void flatten() { + String yaml = "admin:\n" + + " users:\n" + + " -\n" + + " email: \"joe@gmail.com\"\n" + + " username: \"joe\"\n" + + " password: \"123456\"\n" + + " roles:\n" + + " - \"Moderator\"\n" + + " - \"Admin\"\n" + + " -\n" + + " email: \"jack@gmail.com\"\n" + + " username: \"jack\"\n" + + " password: \"654321\"\n" + + " roles:\n" + + " - \"Moderator\"\n"; + + YamlConfigSource source = new YamlConfigSource("yaml", yaml); + String value = source.getValue("admin.users"); Users users = new UserConverter().convert(value); assertEquals(2, users.getUsers().size()); assertEquals(users.users.get(0).getEmail(), "joe@gmail.com"); assertEquals(users.users.get(0).getRoles(), Stream.of("Moderator", "Admin").collect(toList())); - assertEquals("joe@gmail.com", yaml.getValue("admin.users[0].email")); + assertEquals("joe@gmail.com", source.getValue("admin.users[0].email")); } @Test - void profiles() throws Exception { - YamlConfigSource yaml = new YamlConfigSource(YamlConfigSourceTest.class.getResource("/example-profiles.yml")); - - assertEquals("default", yaml.getValue("foo.bar")); - assertEquals("dev", yaml.getValue("%dev.foo.bar")); - assertEquals("prod", yaml.getValue("%prod.foo.bar")); + void profiles() { + String yaml = "---\n" + + "foo:\n" + + " bar:\n" + + " default\n" + + "---\n" + + "\"%dev\":\n" + + " foo:\n" + + " bar:\n" + + " dev\n" + + "---\n" + + "\"%prod\":\n" + + " foo:\n" + + " bar:\n" + + " prod\n"; + + YamlConfigSource source = new YamlConfigSource("yaml", yaml); + assertEquals("default", source.getValue("foo.bar")); + assertEquals("dev", source.getValue("%dev.foo.bar")); + assertEquals("prod", source.getValue("%prod.foo.bar")); } @Test @@ -120,13 +150,29 @@ void indentSpaces() { } @Test - void config() throws Exception { + void config() { + String yaml = "admin:\n" + + " users:\n" + + " -\n" + + " email: \"joe@gmail.com\"\n" + + " username: \"joe\"\n" + + " password: \"123456\"\n" + + " roles:\n" + + " - \"Moderator\"\n" + + " - \"Admin\"\n" + + " -\n" + + " email: \"jack@gmail.com\"\n" + + " username: \"jack\"\n" + + " password: \"654321\"\n" + + " roles:\n" + + " - \"Moderator\"\n"; + SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(new YamlConfigSource(YamlConfigSourceTest.class.getResource("/example-216.yml"))) + .withSources(new YamlConfigSource("yaml", yaml)) .withConverter(Users.class, 100, new UserConverter()) .build(); - final Users users = config.getValue("admin.users", Users.class); + Users users = config.getValue("admin.users", Users.class); assertEquals(2, users.getUsers().size()); assertEquals(users.users.get(0).getEmail(), "joe@gmail.com"); assertEquals(users.users.get(0).getRoles(), Stream.of("Moderator", "Admin").collect(toList())); @@ -135,13 +181,41 @@ void config() throws Exception { } @Test - void propertyNames() throws Exception { + void propertyNames() { + String yaml = "quarkus:\n" + + " http:\n" + + " port: 8081\n" + + " ssl-port: 2443\n" + + " cors:\n" + + " ~: true\n" + + " access-control-max-age: 24H\n" + + " exposed-headers: \"SOME-HEADER\"\n" + + " methods: GET,PUT,POST,DELETE,OPTIONS\n" + + " ssl:\n" + + " protocols:\n" + + " - TLSv1.2\n" + + " - TLSv1.3\n" + + " cipher-suites:\n" + + " - TLS_AES_128_GCM_SHA256\n" + + " - TLS_AES_256_GCM_SHA384\n" + + " - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\n" + + " - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\n" + + " - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\n" + + " - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\n" + + " swagger-ui:\n" + + " always-include: true\n" + + "\n" + + " jib:\n" + + " jvm-arguments:\n" + + " - \"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005\"\n" + + " - \"-Dquarkus.http.host=0.0.0.0\"\n" + + " - \"-Djava.util.logging.manager=org.jboss.logmanager.LogManager\"\n"; + SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(new YamlConfigSource(YamlConfigSourceTest.class.getResource("/example.yml"))) + .withSources(new YamlConfigSource("yaml", yaml)) .build(); - final List propertyNames = StreamSupport.stream(config.getPropertyNames().spliterator(), false) - .collect(toList()); + List propertyNames = StreamSupport.stream(config.getPropertyNames().spliterator(), false).collect(toList()); assertTrue(propertyNames.contains("quarkus.http.port")); assertTrue(propertyNames.contains("quarkus.http.ssl-port")); @@ -151,14 +225,20 @@ void propertyNames() throws Exception { } @Test - void quotedProperties() throws Exception { + void quotedProperties() { + String yaml = "quarkus:\n" + + " log:\n" + + " category:\n" + + " \"liquibase.changelog.ChangeSet\":\n" + + " level: INFO\n" + + " \"liquibase\":\n" + + " level: WARN\n"; + SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources( - new YamlConfigSource(YamlConfigSourceTest.class.getResource("/example-quotes.yml"))) + .withSources(new YamlConfigSource("yaml", yaml)) .build(); - final List propertyNames = StreamSupport.stream(config.getPropertyNames().spliterator(), false) - .collect(toList()); + List propertyNames = StreamSupport.stream(config.getPropertyNames().spliterator(), false).collect(toList()); assertTrue(propertyNames.contains("quarkus.log.category.liquibase.level")); assertTrue(propertyNames.contains("quarkus.log.category.\"liquibase.changelog.ChangeSet\".level")); @@ -166,9 +246,38 @@ void quotedProperties() throws Exception { } @Test - void commas() throws Exception { + void commas() { + String yaml = "quarkus:\n" + + " http:\n" + + " port: 8081\n" + + " ssl-port: 2443\n" + + " cors:\n" + + " ~: true\n" + + " access-control-max-age: 24H\n" + + " exposed-headers: \"SOME-HEADER\"\n" + + " methods: GET,PUT,POST,DELETE,OPTIONS\n" + + " ssl:\n" + + " protocols:\n" + + " - TLSv1.2\n" + + " - TLSv1.3\n" + + " cipher-suites:\n" + + " - TLS_AES_128_GCM_SHA256\n" + + " - TLS_AES_256_GCM_SHA384\n" + + " - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\n" + + " - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\n" + + " - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\n" + + " - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\n" + + " swagger-ui:\n" + + " always-include: true\n" + + "\n" + + " jib:\n" + + " jvm-arguments:\n" + + " - \"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005\"\n" + + " - \"-Dquarkus.http.host=0.0.0.0\"\n" + + " - \"-Djava.util.logging.manager=org.jboss.logmanager.LogManager\"\n"; + SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(new YamlConfigSource(YamlConfigSourceTest.class.getResource("/example.yml"))) + .withSources(new YamlConfigSource("yaml", yaml)) .build(); String[] values = config.getValue("quarkus.jib.jvm-arguments", String[].class); @@ -178,9 +287,18 @@ void commas() throws Exception { @Test void intKeys() { + String yaml = "storefront:\n" + + " path:\n" + + " appConfig:\n" + + " 1: /storefront/appConfig/*\n" + + " 2: /storefront/storefront/appConfig/*\n" + + " training:\n" + + " 1: /storefront/training/*\n" + + " 2: /storefront/storefront/training/*\n"; + try { new SmallRyeConfigBuilder() - .withSources(new YamlConfigSource(YamlConfigSourceTest.class.getResource("/example-int-keys.yml"))) + .withSources(new YamlConfigSource("yaml", yaml)) .build(); } catch (Exception e) { fail(e); @@ -188,9 +306,25 @@ void intKeys() { } @Test - void mapping() throws Exception { + void mapping() { + String yaml = "admin:\n" + + " users:\n" + + " -\n" + + " email: \"joe@gmail.com\"\n" + + " username: \"joe\"\n" + + " password: \"123456\"\n" + + " roles:\n" + + " - \"Moderator\"\n" + + " - \"Admin\"\n" + + " -\n" + + " email: \"jack@gmail.com\"\n" + + " username: \"jack\"\n" + + " password: \"654321\"\n" + + " roles:\n" + + " - \"Moderator\"\n"; + SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(new YamlConfigSource(YamlConfigSourceTest.class.getResource("/example-216.yml"))) + .withSources(new YamlConfigSource("yaml", yaml)) .withMapping(UsersMapping.class, "admin") .build(); @@ -203,9 +337,24 @@ void mapping() throws Exception { } @Test - void mappingCollections() throws Exception { + void mappingCollections() { + String yaml = "application:\n" + + " environments:\n" + + " - name: dev\n" + + " services:\n" + + " - name: batch\n" + + " - name: rest\n" + + " - name: prod\n" + + " services:\n" + + " - name: web\n" + + " - name: batch\n" + + " - name: rest\n" + + " images:\n" + + " - base\n" + + " - jdk\n"; + SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(new YamlConfigSource(YamlConfigSourceTest.class.getResource("/example-collections.yml"))) + .withSources(new YamlConfigSource("yaml", yaml)) .withMapping(Application.class, "application") .build(); @@ -225,9 +374,26 @@ void mappingCollections() throws Exception { } @Test - void optional() throws Exception { + void optional() { + String yaml = "---\n" + + "\"%base\":\n" + + " server:\n" + + " name: localhost\n" + + " config:\n" + + " server: localhost\n" + + " port: 1143\n" + + " user: user\n" + + " password: password\n" + + " version:\n" + + " major: 16\n" + + " minor: 0\n" + + "---\n" + + "\"%empty\":\n" + + " server:\n" + + " name: localhost\n"; + SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(new YamlConfigSource(YamlConfigSourceTest.class.getResource("/optional.yml"))) + .withSources(new YamlConfigSource("yaml", yaml)) .withMapping(Server.class, "server") .withProfile("base") .build(); @@ -243,7 +409,7 @@ void optional() throws Exception { assertEquals(0, server.config().get().version().minor()); config = new SmallRyeConfigBuilder() - .withSources(new YamlConfigSource(YamlConfigSourceTest.class.getResource("/optional.yml"))) + .withSources(new YamlConfigSource("yaml", yaml)) .withMapping(Server.class, "server") .withProfile("empty") .build(); @@ -255,10 +421,12 @@ void optional() throws Exception { @Test void timestampConverters() { + String yaml = "date: 2010-10-10\n" + + "dateTime: 2010-10-10T10:10:10\n" + + "zonedDateTime: 2020-10-10T10:10:10-05:00"; + SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(new YamlConfigSource("yaml", "date: 2010-10-10\n" + - "dateTime: 2010-10-10T10:10:10\n" + - "zonedDateTime: 2020-10-10T10:10:10-05:00")) + .withSources(new YamlConfigSource("yaml", yaml)) .build(); assertEquals(LocalDate.of(2010, 10, 10), config.getValue("date", LocalDate.class)); diff --git a/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlLocationConfigSourceFactoryTest.java b/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlLocationConfigSourceFactoryTest.java index 14f3c1b00..641ca73d4 100644 --- a/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlLocationConfigSourceFactoryTest.java +++ b/sources/yaml/src/test/java/io/smallrye/config/source/yaml/YamlLocationConfigSourceFactoryTest.java @@ -1,14 +1,13 @@ package io.smallrye.config.source.yaml; import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_LOCATIONS; +import static java.util.logging.Level.ALL; import static java.util.stream.StreamSupport.stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.logging.Level; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -19,7 +18,7 @@ class YamlLocationConfigSourceFactoryTest { @RegisterExtension - static LogCapture logCapture = LogCapture.with(logRecord -> logRecord.getMessage().startsWith("SRCFG"), Level.ALL); + static LogCapture logCapture = LogCapture.with(logRecord -> logRecord.getMessage().startsWith("SRCFG01005"), ALL); @BeforeEach void setUp() { @@ -41,15 +40,15 @@ void systemFolder() { assertEquals("1234", config.getRawValue("my.prop")); assertEquals("5678", config.getRawValue("more.prop")); - assertEquals(11, countSources(config)); + assertEquals(2, countSources(config)); } @Test void webResource() { SmallRyeConfig config = buildConfig( - "https://raw.githubusercontent.com/smallrye/smallrye-config/main/sources/yaml/src/test/resources/example-profiles.yml"); + "https://raw.githubusercontent.com/smallrye/smallrye-config/main/sources/yaml/src/test/resources/more.yml"); - assertEquals("default", config.getRawValue("foo.bar")); + assertEquals("5678", config.getRawValue("more.prop")); assertEquals(1, countSources(config)); } @@ -64,11 +63,11 @@ void classpath() { @Test void all() { SmallRyeConfig config = buildConfig("./src/test/resources", - "https://raw.githubusercontent.com/smallrye/smallrye-config/main/sources/yaml/src/test/resources/example-profiles.yml"); + "https://raw.githubusercontent.com/smallrye/smallrye-config/main/sources/yaml/src/test/resources/more.yml"); assertEquals("1234", config.getRawValue("my.prop")); assertEquals("5678", config.getRawValue("more.prop")); - assertEquals(12, countSources(config)); + assertEquals(3, countSources(config)); } @Test @@ -88,7 +87,7 @@ void noPropertiesFile() { @Test void invalidWebResource() { - assertThrows(IllegalStateException.class, + assertThrows(IllegalArgumentException.class, () -> buildConfig("https://raw.githubusercontent.com/smallrye/smallrye-config/notfound.yml")); buildConfig("https://github.com/smallrye/smallrye-config/blob/3cc4809734d7fbd03852a20b5870ca743a2427bc/pom.xml"); } @@ -108,6 +107,11 @@ void warningNoMessageIfAnySourceFound() { assertTrue(logCapture.records().isEmpty()); } + @Test + void missingFile() { + assertThrows(IllegalArgumentException.class, () -> buildConfig("file:/not-found.yaml")); + } + private static SmallRyeConfig buildConfig(String... locations) { return new SmallRyeConfigBuilder() .addDiscoveredSources() diff --git a/sources/yaml/src/test/resources/example-216.yml b/sources/yaml/src/test/resources/example-216.yml deleted file mode 100644 index 7ff71e7d5..000000000 --- a/sources/yaml/src/test/resources/example-216.yml +++ /dev/null @@ -1,15 +0,0 @@ -admin: - users: - - - email: "joe@gmail.com" - username: "joe" - password: "123456" - roles: - - "Moderator" - - "Admin" - - - email: "jack@gmail.com" - username: "jack" - password: "654321" - roles: - - "Moderator" diff --git a/sources/yaml/src/test/resources/example-collections.yml b/sources/yaml/src/test/resources/example-collections.yml deleted file mode 100644 index 9a6d7d360..000000000 --- a/sources/yaml/src/test/resources/example-collections.yml +++ /dev/null @@ -1,14 +0,0 @@ -application: - environments: - - name: dev - services: - - name: batch - - name: rest - - name: prod - services: - - name: web - - name: batch - - name: rest - images: - - base - - jdk diff --git a/sources/yaml/src/test/resources/example-int-keys.yml b/sources/yaml/src/test/resources/example-int-keys.yml deleted file mode 100644 index a6aad5e63..000000000 --- a/sources/yaml/src/test/resources/example-int-keys.yml +++ /dev/null @@ -1,8 +0,0 @@ -storefront: - path: - appConfig: - 1: /storefront/appConfig/* - 2: /storefront/storefront/appConfig/* - training: - 1: /storefront/training/* - 2: /storefront/storefront/training/* diff --git a/sources/yaml/src/test/resources/example-map-list.yml b/sources/yaml/src/test/resources/example-map-list.yml deleted file mode 100644 index 7949bb64e..000000000 --- a/sources/yaml/src/test/resources/example-map-list.yml +++ /dev/null @@ -1,40 +0,0 @@ -map: - roles: - hokage: - - - name: Senju Hashirama - nature: Earth,Water - - - name: Senju Tobirama - nature: Water - - - name: Sarotobi Hiruzen - nature: Fire - - - name: Namikaze Minato - nature: Fire,Wind,Lightning - - - name: Tsunade - nature: Lightning - - - name: Hatake Kakashi - nature: Lightning - - - name: Uzumaki Naruto - nature: Wind - jutsus: - - - name: Rasengan - rank: A - - - name: Bijūdama - rank: S - teams: - "Team Kakashi": - - Hatake Kakashi - - Uchiha Sasuke - - Haruno Sakura - "Kazekage Rescue Team": - - Hatake Kakashi - - Chiyo - - Haruno Sakura diff --git a/sources/yaml/src/test/resources/example-mapping.yml b/sources/yaml/src/test/resources/example-mapping.yml deleted file mode 100644 index 4217c12cc..000000000 --- a/sources/yaml/src/test/resources/example-mapping.yml +++ /dev/null @@ -1,59 +0,0 @@ -proxies: - - type: mssql - name: first - input: - pool: - max_pool_size: 100 - expires_in_seconds: 60 - mssql: - server: 192.168.1.2 - port: 1434 - user: 'test' - password: 'test' - read: - pool: - max_pool_size: 100 - expires_in_seconds: 60 - regex: '(?i)^\s*select.*' - mssql: - server: 127.0.0.1 - port: 1433 - user: 'sa' - password: 'Test!1234' - write: - pool: - max_pool_size: 100 - expires_in_seconds: 60 - mssql: - server: 127.0.0.1 - port: 1433 - user: 'sa' - password: 'Test!1234' - - type: mssql - name: second - input: - mssql: - server: 192.168.1.2 - port: 1435 - user: 'test' - password: 'test' - pool: - max_pool_size: 0 - expires_in_seconds: 0 - encryption: - level: 'SUPPORTED' - keystore: - location: 'my-location' - read: - regex: '(?i)^\s*select.*' - mssql: - server: 127.0.0.1 - port: 1433 - user: 'sa' - password: 'Test!1234' - write: - mssql: - server: 127.0.0.1 - port: 1433 - user: 'sa' - password: 'Test!1234' diff --git a/sources/yaml/src/test/resources/example-profiles.yml b/sources/yaml/src/test/resources/example-profiles.yml deleted file mode 100644 index b4226a8c8..000000000 --- a/sources/yaml/src/test/resources/example-profiles.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -foo: - bar: - default ---- -"%dev": - foo: - bar: - dev ---- -"%prod": - foo: - bar: - prod diff --git a/sources/yaml/src/test/resources/example-quotes.yml b/sources/yaml/src/test/resources/example-quotes.yml deleted file mode 100644 index 3a44aa25b..000000000 --- a/sources/yaml/src/test/resources/example-quotes.yml +++ /dev/null @@ -1,7 +0,0 @@ -quarkus: - log: - category: - "liquibase.changelog.ChangeSet": - level: INFO - "liquibase": - level: WARN diff --git a/sources/yaml/src/test/resources/example.yml b/sources/yaml/src/test/resources/example.yml deleted file mode 100644 index bab9e40ed..000000000 --- a/sources/yaml/src/test/resources/example.yml +++ /dev/null @@ -1,28 +0,0 @@ -quarkus: - http: - port: 8081 - ssl-port: 2443 - cors: - ~: true - access-control-max-age: 24H - exposed-headers: "SOME-HEADER" - methods: GET,PUT,POST,DELETE,OPTIONS - ssl: - protocols: - - TLSv1.2 - - TLSv1.3 - cipher-suites: - - TLS_AES_128_GCM_SHA256 - - TLS_AES_256_GCM_SHA384 - - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 - - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 - - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 - - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - swagger-ui: - always-include: true - - jib: - jvm-arguments: - - "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" - - "-Dquarkus.http.host=0.0.0.0" - - "-Djava.util.logging.manager=org.jboss.logmanager.LogManager" diff --git a/sources/yaml/src/test/resources/optional.yml b/sources/yaml/src/test/resources/optional.yml deleted file mode 100644 index e8288a5b6..000000000 --- a/sources/yaml/src/test/resources/optional.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -"%base": - server: - name: localhost - config: - server: localhost - port: 1143 - user: user - password: password - version: - major: 16 - minor: 0 ---- -"%empty": - server: - name: localhost diff --git a/sources/zookeeper/pom.xml b/sources/zookeeper/pom.xml index f3dc5c04a..c9e2b88b1 100644 --- a/sources/zookeeper/pom.xml +++ b/sources/zookeeper/pom.xml @@ -20,16 +20,16 @@ io.smallrye.config smallrye-config-parent - 2.11.1 + 3.7.1 ../../pom.xml smallrye-config-source-zookeeper - SmallRye: ZooKeeper ConfigSource + SmallRye Config: ConfigSource - ZooKeeper - 5.3.0 + 5.6.0 diff --git a/sources/zookeeper/src/test/java/io/smallrye/config/source/zookeeper/tests/ZooKeeperConfigSourceTest.java b/sources/zookeeper/src/test/java/io/smallrye/config/source/zookeeper/tests/ZooKeeperConfigSourceTest.java index 5585f4692..877258b61 100644 --- a/sources/zookeeper/src/test/java/io/smallrye/config/source/zookeeper/tests/ZooKeeperConfigSourceTest.java +++ b/sources/zookeeper/src/test/java/io/smallrye/config/source/zookeeper/tests/ZooKeeperConfigSourceTest.java @@ -16,14 +16,13 @@ import java.util.Set; import java.util.logging.Logger; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.retry.ExponentialBackoffRetry; import org.apache.curator.test.TestingServer; -import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.weld.junit5.WeldInitiator; @@ -34,6 +33,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.inject.ConfigExtension; /** @@ -92,20 +92,20 @@ static void tearDownClass() throws Exception { void testGettingProperty() { logger.info("ZooKeeperConfigSourceTest.testGettingProperty"); - Config cfg = ConfigProvider.getConfig(); + SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class); //Check that the ZK ConfigSource will work - assertNotNull(cfg.getValue("io.smallrye.configsource.zookeeper.url", String.class)); + assertNotNull(config.getValue("io.smallrye.configsource.zookeeper.url", String.class)); //Check that a property doesn't exist yet try { - cfg.getValue(PROPERTY_NAME, String.class); + config.getValue(PROPERTY_NAME, String.class); fail("Property " + PROPERTY_NAME + " should not exist"); } catch (NoSuchElementException ignored) { } //Check that the optional version of the property is not present - assertFalse(cfg.getOptionalValue(PROPERTY_NAME, String.class).isPresent()); + assertFalse(config.getOptionalValue(PROPERTY_NAME, String.class).isPresent()); //setup the property in ZK try { curatorClient.createContainers(ZK_KEY); @@ -115,10 +115,10 @@ void testGettingProperty() { } //check the property can be optained by a property - assertEquals(PROPERTY_VALUE, cfg.getValue(PROPERTY_NAME, String.class)); + assertEquals(PROPERTY_VALUE, config.getValue(PROPERTY_NAME, String.class)); Set propertyNames = new HashSet<>(); - cfg.getPropertyNames().forEach(propertyNames::add); + config.getLatestPropertyNames().forEach(propertyNames::add); assertTrue(propertyNames.contains(PROPERTY_NAME)); } diff --git a/testsuite/extra/pom.xml b/testsuite/extra/pom.xml index 42815d088..6bb345afe 100644 --- a/testsuite/extra/pom.xml +++ b/testsuite/extra/pom.xml @@ -22,15 +22,15 @@ io.smallrye.config smallrye-config-testsuite - 2.11.1 + 3.7.1 smallrye-config-test-extra - SmallRye: MicroProfile Config Test Suite - Extra + SmallRye Config: Test Suite - Extra - 1.7.10 + 1.9.23 @@ -53,6 +53,16 @@ smallrye-config-source-yaml ${project.version} + + io.smallrye.config + smallrye-config-crypto + ${project.version} + + + io.smallrye.config + smallrye-config-jasypt + ${project.version} + @@ -103,7 +113,7 @@ test-compile - test-compile + generate-test-sources test-compile @@ -129,7 +139,6 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M7 @@ -143,12 +152,12 @@ org.apache.maven.surefire surefire-junit-platform - 3.0.0-M7 + ${version.surefire.plugin} org.apache.maven.surefire surefire-testng - 3.0.0-M7 + ${version.surefire.plugin} diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/builder/CustomConfigBuilder.java b/testsuite/extra/src/test/java/io/smallrye/config/test/builder/CustomConfigBuilder.java new file mode 100644 index 000000000..257c847ed --- /dev/null +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/builder/CustomConfigBuilder.java @@ -0,0 +1,11 @@ +package io.smallrye.config.test.builder; + +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilderCustomizer; + +public class CustomConfigBuilder implements SmallRyeConfigBuilderCustomizer { + @Override + public void configBuilder(final SmallRyeConfigBuilder builder) { + builder.withDefaultValue("from.custom.builder", "1234"); + } +} diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/builder/CustomOneConfigBuilder.java b/testsuite/extra/src/test/java/io/smallrye/config/test/builder/CustomOneConfigBuilder.java new file mode 100644 index 000000000..ffff0a0ba --- /dev/null +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/builder/CustomOneConfigBuilder.java @@ -0,0 +1,16 @@ +package io.smallrye.config.test.builder; + +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilderCustomizer; + +public class CustomOneConfigBuilder implements SmallRyeConfigBuilderCustomizer { + @Override + public void configBuilder(final SmallRyeConfigBuilder builder) { + builder.withDefaultValue("one", "one").addDefaultSources(); + } + + @Override + public int priority() { + return 1; + } +} diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/builder/CustomTwoConfigBuilder.java b/testsuite/extra/src/test/java/io/smallrye/config/test/builder/CustomTwoConfigBuilder.java new file mode 100644 index 000000000..1ae183956 --- /dev/null +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/builder/CustomTwoConfigBuilder.java @@ -0,0 +1,19 @@ +package io.smallrye.config.test.builder; + +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilderCustomizer; + +public class CustomTwoConfigBuilder implements SmallRyeConfigBuilderCustomizer { + @Override + public void configBuilder(final SmallRyeConfigBuilder builder) { + builder.withDefaultValue("one", "two"); + if (builder.isAddDefaultSources()) { + builder.withDefaultValue("addDefaultSources", "true"); + } + } + + @Override + public int priority() { + return 2; + } +} diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/builder/SmallRyeConfigBuilderCustomizerTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/builder/SmallRyeConfigBuilderCustomizerTest.java new file mode 100644 index 000000000..a43d9272d --- /dev/null +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/builder/SmallRyeConfigBuilderCustomizerTest.java @@ -0,0 +1,89 @@ +package io.smallrye.config.test.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.stream.Stream; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.exporter.ZipExporter; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; + +class SmallRyeConfigBuilderCustomizerTest { + @Test + void builder() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withCustomizers(new CustomConfigBuilder()) + .build(); + + assertEquals("1234", config.getRawValue("from.custom.builder")); + } + + @Test + void discoveredBuilder(@TempDir Path tempDir) throws Exception { + JavaArchive serviceJar = ShrinkWrap + .create(JavaArchive.class, "service.jar") + .addAsManifestResource(new StringAsset("io.smallrye.config.test.builder.CustomConfigBuilder"), + "services/io.smallrye.config.SmallRyeConfigBuilderCustomizer"); + + Path servidePath = tempDir.resolve("resources-one.jar"); + serviceJar.as(ZipExporter.class).exportTo(servidePath.toFile()); + + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + + try (URLClassLoader urlClassLoader = urlClassLoader(contextClassLoader, "jar:" + servidePath.toUri() + "!/")) { + Thread.currentThread().setContextClassLoader(urlClassLoader); + + SmallRyeConfig config = new SmallRyeConfigBuilder().addDiscoveredCustomizers().build(); + + assertEquals("1234", config.getRawValue("from.custom.builder")); + } finally { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + + @Test + void priority(@TempDir Path tempDir) throws Exception { + JavaArchive serviceJar = ShrinkWrap + .create(JavaArchive.class, "service.jar") + .addAsManifestResource(new StringAsset( + "io.smallrye.config.test.builder.CustomOneConfigBuilder\n" + + "io.smallrye.config.test.builder.CustomTwoConfigBuilder\n"), + "services/io.smallrye.config.SmallRyeConfigBuilderCustomizer"); + + Path servidePath = tempDir.resolve("resources-one.jar"); + serviceJar.as(ZipExporter.class).exportTo(servidePath.toFile()); + + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + + try (URLClassLoader urlClassLoader = urlClassLoader(contextClassLoader, "jar:" + servidePath.toUri() + "!/")) { + Thread.currentThread().setContextClassLoader(urlClassLoader); + + SmallRyeConfig config = new SmallRyeConfigBuilder().addDiscoveredCustomizers().build(); + + assertEquals("two", config.getRawValue("one")); + assertEquals("true", config.getRawValue("addDefaultSources")); + } finally { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + + private static URLClassLoader urlClassLoader(ClassLoader parent, String... urls) { + return new URLClassLoader(Stream.of(urls).map(spec -> { + try { + return new URL(spec); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + }).toArray(URL[]::new), parent); + } +} diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/collections/CollectionBean.java b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/CollectionBean.java index 004ec852c..ad3d73873 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/collections/CollectionBean.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/CollectionBean.java @@ -19,8 +19,8 @@ import java.util.List; import java.util.Set; -import javax.inject.Inject; -import javax.inject.Provider; +import jakarta.inject.Inject; +import jakarta.inject.Provider; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/collections/CollectionWithConfiguredValueTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/CollectionWithConfiguredValueTest.java index 413b15670..3c93bd735 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/collections/CollectionWithConfiguredValueTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/CollectionWithConfiguredValueTest.java @@ -26,12 +26,11 @@ import java.util.NoSuchElementException; import java.util.Set; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.testng.Arquillian; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.jboss.shrinkwrap.api.spec.WebArchive; @@ -49,9 +48,8 @@ public static WebArchive deploy() { JavaArchive testJar = ShrinkWrap .create(JavaArchive.class, "CollectionWithConfiguredValueTest.jar") .addClasses(CollectionWithConfiguredValueTest.class, CollectionBean.class) - .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml") - .addAsManifestResource(new StringAsset( - "myPets=snake,ox"), "microprofile-config.properties") + .addAsManifestResource("beans.xml") + .addAsManifestResource(new StringAsset("myPets=snake,ox"), "microprofile-config.properties") .as(JavaArchive.class); return ShrinkWrap .create(WebArchive.class, "CollectionWithConfiguredValueTest.war") diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/collections/CollectionWithDefaultValueTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/CollectionWithDefaultValueTest.java index e6b420fd3..8e76785e2 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/collections/CollectionWithDefaultValueTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/CollectionWithDefaultValueTest.java @@ -24,12 +24,11 @@ import java.util.List; import java.util.Set; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.testng.Arquillian; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.testng.annotations.Test; @@ -47,7 +46,7 @@ public static WebArchive deploy() { JavaArchive testJar = ShrinkWrap .create(JavaArchive.class, "CollectionWithDefaultValueTest.jar") .addClasses(CollectionWithDefaultValueTest.class, CollectionBean.class) - .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml") + .addAsManifestResource("beans.xml") .as(JavaArchive.class); return ShrinkWrap .create(WebArchive.class, "CollectionWithDefaultValueTest.war") diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/collections/KotlinCollectionsBean.kt b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/KotlinCollectionsBean.kt new file mode 100644 index 000000000..218e02134 --- /dev/null +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/KotlinCollectionsBean.kt @@ -0,0 +1,20 @@ +package io.smallrye.config.test.collections + +import org.eclipse.microprofile.config.inject.ConfigProperty +import jakarta.enterprise.context.Dependent +import jakarta.inject.Inject + +@Dependent +class KotlinCollectionsBean { + @Inject + @ConfigProperty(name = "property.list") + lateinit var typeList: List + + @Inject + @ConfigProperty(name = "property.single") + lateinit var singleType: MyType +} + +class MyType(val value: String) { + +} diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/collections/KotlinCollectionsBeanTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/KotlinCollectionsBeanTest.java new file mode 100644 index 000000000..ca0ccb62f --- /dev/null +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/KotlinCollectionsBeanTest.java @@ -0,0 +1,37 @@ +package io.smallrye.config.test.collections; + +import static org.testng.Assert.assertEquals; + +import jakarta.inject.Inject; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.testng.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.testng.annotations.Test; + +public class KotlinCollectionsBeanTest extends Arquillian { + @Deployment + public static WebArchive deploy() { + return ShrinkWrap + .create(WebArchive.class) + .addClasses(CollectionBean.class) + .addClasses(KotlinCollectionsBean.class, KotlinCollectionsBeanTest.class) + .addAsManifestResource("beans.xml") + .addAsResource(new StringAsset("property.list=1,2,3\n" + + "property.single=1234\n"), + "META-INF/microprofile-config.properties"); + } + + @Inject + KotlinCollectionsBean kotlinCollectionsBean; + + @Test + public void kotlinCollections() { + assertEquals(kotlinCollectionsBean.typeList.get(0).getValue(), "1"); + assertEquals(kotlinCollectionsBean.typeList.get(1).getValue(), "2"); + assertEquals(kotlinCollectionsBean.typeList.get(2).getValue(), "3"); + assertEquals(kotlinCollectionsBean.singleType.getValue(), "1234"); + } +} diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/collections/broken/CollectionWithNoDefaultValueBean.java b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/broken/CollectionWithNoDefaultValueBean.java index 4b881ddae..0ecf3e0e1 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/collections/broken/CollectionWithNoDefaultValueBean.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/broken/CollectionWithNoDefaultValueBean.java @@ -19,7 +19,7 @@ import java.util.List; import java.util.Set; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/collections/broken/CollectionWithNoDefaultValueTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/broken/CollectionWithNoDefaultValueTest.java index ad7b538af..05a5d2fb1 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/collections/broken/CollectionWithNoDefaultValueTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/collections/broken/CollectionWithNoDefaultValueTest.java @@ -15,13 +15,12 @@ */ package io.smallrye.config.test.collections.broken; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.container.test.api.ShouldThrowException; import org.jboss.arquillian.testng.Arquillian; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.jboss.weld.exceptions.DeploymentException; @@ -37,7 +36,7 @@ public static WebArchive deploy() { JavaArchive testJar = ShrinkWrap .create(JavaArchive.class, "CollectionWithNoDefaultValueTest.jar") .addClasses(CollectionWithNoDefaultValueTest.class, CollectionWithNoDefaultValueBean.class) - .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml") + .addAsManifestResource("beans.xml") .as(JavaArchive.class); return ShrinkWrap .create(WebArchive.class, "CollectionWithNoDefaultValueTest.war") diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/converter/ConverterBean.java b/testsuite/extra/src/test/java/io/smallrye/config/test/converter/ConverterBean.java index c36e23dcc..bf850cdd6 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/converter/ConverterBean.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/converter/ConverterBean.java @@ -15,7 +15,7 @@ */ package io.smallrye.config.test.converter; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/converter/ConverterTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/converter/ConverterTest.java index 25003fc58..2d2c7d419 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/converter/ConverterTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/converter/ConverterTest.java @@ -17,13 +17,12 @@ import static org.testng.Assert.assertEquals; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.spi.Converter; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.testng.Arquillian; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.testng.annotations.Test; @@ -41,7 +40,7 @@ public static WebArchive deploy() { .addClasses(ConverterTest.class, ConverterBean.class) .addClass(IntConverter.class) .addAsServiceProvider(Converter.class, IntConverter.class) - .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml"); + .addAsWebInfResource("beans.xml"); } @Inject diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/converter/IntConverter.java b/testsuite/extra/src/test/java/io/smallrye/config/test/converter/IntConverter.java index e642c3fef..7f0f252f9 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/converter/IntConverter.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/converter/IntConverter.java @@ -15,7 +15,7 @@ */ package io.smallrye.config.test.converter; -import javax.annotation.Priority; +import jakarta.annotation.Priority; import org.eclipse.microprofile.config.spi.Converter; diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/location/DotEnvTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/location/DotEnvTest.java new file mode 100644 index 000000000..3b611e988 --- /dev/null +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/location/DotEnvTest.java @@ -0,0 +1,56 @@ +package io.smallrye.config.test.location; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.FileOutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import io.smallrye.config.DotEnvConfigSourceProvider; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; + +public class DotEnvTest { + @Test + void dotEnv(@TempDir Path tempDir) throws Exception { + Properties dotEnv = new Properties(); + dotEnv.setProperty("FOO_BAR", "value"); + try (FileOutputStream out = new FileOutputStream(tempDir.resolve(".env").toFile())) { + dotEnv.store(out, null); + } + + String previousUserDir = System.getProperty("user.dir"); + System.setProperty("user.dir", tempDir.toString()); + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(new DotEnvConfigSourceProvider()) + .build(); + + assertEquals("value", config.getRawValue("foo.bar")); + + System.setProperty("user.dir", previousUserDir); + } + + @Test + void dotEnvFolder(@TempDir Path tempDir) throws Exception { + Path dotEnvFolder = tempDir.resolve(".env"); + Files.createDirectories(dotEnvFolder); + Files.createDirectories(dotEnvFolder.resolve("foo")); + + String previousUserDir = System.getProperty("user.dir"); + System.setProperty("user.dir", tempDir.toString()); + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(new DotEnvConfigSourceProvider()) + .build(); + + assertNull(config.getRawValue("foo.bar")); + + System.setProperty("user.dir", previousUserDir); + } +} diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/location/LocationConfigTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/location/LocationConfigTest.java index e511a1f45..009e706d4 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/location/LocationConfigTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/location/LocationConfigTest.java @@ -2,7 +2,7 @@ import static org.testng.Assert.assertEquals; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.Config; import org.jboss.arquillian.container.test.api.Deployment; diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/location/PropertiesLocationTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/location/PropertiesLocationTest.java index aff6d3167..9c6b2fb18 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/location/PropertiesLocationTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/location/PropertiesLocationTest.java @@ -429,6 +429,40 @@ void illegalChars(@TempDir Path tempDir) throws Exception { } } + @Test + void mixedExtensions(@TempDir Path tempDir) throws Exception { + JavaArchive jar = ShrinkWrap + .create(JavaArchive.class, "resources.jar") + .addAsResource(new StringAsset("my:\n" + + " prop:\n" + + " one: 1234\n"), "resources.yml") + .addAsResource(new StringAsset("my:\n" + + " prop:\n" + + " one: 5678\n"), "resources-prod.yaml"); + + Path filePath = tempDir.resolve("resources.jar"); + jar.as(ZipExporter.class).exportTo(filePath.toFile()); + + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { + new URL("jar:" + filePath.toUri() + "!/") + }, contextClassLoader)) { + Thread.currentThread().setContextClassLoader(urlClassLoader); + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDiscoveredSources() + .addDefaultInterceptors() + .withProfile("prod") + .withDefaultValue(SMALLRYE_CONFIG_LOCATIONS, "jar:" + filePath.toUri() + "!/resources.yml") + .build(); + + assertEquals("5678", config.getRawValue("my.prop.one")); + assertEquals(2, countSources(config, YamlConfigSource.class)); + } finally { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + private static URLClassLoader urlClassLoader(ClassLoader parent, String... urls) { return new URLClassLoader(Stream.of(urls).map(spec -> { try { diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/InstanceAlone.java b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/InstanceAlone.java index cc212ba57..7f6a32bb0 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/InstanceAlone.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/InstanceAlone.java @@ -22,8 +22,8 @@ package io.smallrye.config.test.provider; -import javax.enterprise.inject.Instance; -import javax.inject.Inject; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/InstanceAloneTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/InstanceAloneTest.java index ecd887506..2c7a9987c 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/InstanceAloneTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/InstanceAloneTest.java @@ -18,13 +18,12 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; -import javax.inject.Inject; -import javax.inject.Provider; +import jakarta.inject.Inject; +import jakarta.inject.Provider; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.testng.Arquillian; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.jboss.shrinkwrap.api.spec.WebArchive; @@ -39,9 +38,8 @@ public static WebArchive deploy() { JavaArchive testJar = ShrinkWrap .create(JavaArchive.class, "ProviderTest.jar") .addClasses(InstanceAloneTest.class, Email.class, InstanceAlone.class) - .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml") - .addAsManifestResource(new StringAsset( - "myEmail=example@smallrye.io"), "microprofile-config.properties") + .addAsManifestResource("beans.xml") + .addAsManifestResource(new StringAsset("myEmail=example@smallrye.io"), "microprofile-config.properties") .as(JavaArchive.class); return ShrinkWrap .create(WebArchive.class, "ProviderTest.war") diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderAlone.java b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderAlone.java index c25ed62a6..03b9e8c8a 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderAlone.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderAlone.java @@ -22,8 +22,8 @@ package io.smallrye.config.test.provider; -import javax.inject.Inject; -import javax.inject.Provider; +import jakarta.inject.Inject; +import jakarta.inject.Provider; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderAloneTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderAloneTest.java index 8bc2d74cb..e22ecd872 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderAloneTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderAloneTest.java @@ -18,13 +18,12 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; -import javax.inject.Inject; -import javax.inject.Provider; +import jakarta.inject.Inject; +import jakarta.inject.Provider; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.testng.Arquillian; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.jboss.shrinkwrap.api.spec.WebArchive; @@ -39,9 +38,8 @@ public static WebArchive deploy() { JavaArchive testJar = ShrinkWrap .create(JavaArchive.class, "ProviderTest.jar") .addClasses(ProviderAloneTest.class, Email.class, ProviderAlone.class) - .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml") - .addAsManifestResource(new StringAsset( - "myEmail=example@smallrye.io"), "microprofile-config.properties") + .addAsManifestResource("beans.xml") + .addAsManifestResource(new StringAsset("myEmail=example@smallrye.io"), "microprofile-config.properties") .as(JavaArchive.class); return ShrinkWrap .create(WebArchive.class, "ProviderTest.war") diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderBean.java b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderBean.java index 6e912cfef..7ef1b63fe 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderBean.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderBean.java @@ -22,8 +22,8 @@ package io.smallrye.config.test.provider; -import javax.inject.Inject; -import javax.inject.Provider; +import jakarta.inject.Inject; +import jakarta.inject.Provider; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderBeanWithList.java b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderBeanWithList.java index 5c93569d0..7e5fea604 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderBeanWithList.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderBeanWithList.java @@ -2,8 +2,8 @@ import java.util.List; -import javax.inject.Inject; -import javax.inject.Provider; +import jakarta.inject.Inject; +import jakarta.inject.Provider; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderTest.java index 066bb030d..2ad903def 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderTest.java @@ -18,13 +18,12 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; -import javax.inject.Inject; -import javax.inject.Provider; +import jakarta.inject.Inject; +import jakarta.inject.Provider; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.testng.Arquillian; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.jboss.shrinkwrap.api.spec.WebArchive; @@ -42,9 +41,8 @@ public static WebArchive deploy() { JavaArchive testJar = ShrinkWrap .create(JavaArchive.class, "ProviderTest.jar") .addClasses(ProviderTest.class, Email.class, ProviderBean.class) - .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml") - .addAsManifestResource(new StringAsset( - "myEmail=example@smallrye.io"), "microprofile-config.properties") + .addAsManifestResource("beans.xml") + .addAsManifestResource(new StringAsset("myEmail=example@smallrye.io"), "microprofile-config.properties") .as(JavaArchive.class); return ShrinkWrap .create(WebArchive.class, "ProviderTest.war") diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderWithListTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderWithListTest.java index 4fb6abc3d..f8cc903f0 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderWithListTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/provider/ProviderWithListTest.java @@ -20,13 +20,12 @@ import java.util.List; -import javax.inject.Inject; -import javax.inject.Provider; +import jakarta.inject.Inject; +import jakarta.inject.Provider; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.testng.Arquillian; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.jboss.shrinkwrap.api.spec.WebArchive; @@ -43,9 +42,8 @@ public static WebArchive deploy() { JavaArchive testJar = ShrinkWrap .create(JavaArchive.class, "ProviderTest.jar") .addClasses(ProviderWithListTest.class, Email.class, ProviderBeanWithList.class) - .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml") - .addAsManifestResource(new StringAsset( - "objectIds=a,b,c\nnumbers=4,5,6"), "microprofile-config.properties") + .addAsManifestResource("beans.xml") + .addAsManifestResource(new StringAsset("objectIds=a,b,c\nnumbers=4,5,6"), "microprofile-config.properties") .as(JavaArchive.class); return ShrinkWrap .create(WebArchive.class, "ProviderTest.war") diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/secrets/MultipleSecretHandlersTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/secrets/MultipleSecretHandlersTest.java new file mode 100644 index 000000000..35ffde16c --- /dev/null +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/secrets/MultipleSecretHandlersTest.java @@ -0,0 +1,34 @@ +package io.smallrye.config.test.secrets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; + +class MultipleSecretHandlersTest { + @Test + void multipleHandlers() { + Map properties = Map.of( + "smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key", + "c29tZWFyYml0cmFyeWNyYXp5c3RyaW5ndGhhdGRvZXNub3RtYXR0ZXI", + "smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode", "true", + "aes-gcm-nopadding.secret", "${aes-gcm-nopadding::DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa_kTliQBpuzTobDZxlg}", + "smallrye.config.secret-handler.jasypt.password", "jasypt", + "smallrye.config.secret-handler.jasypt.algorithm", "PBEWithHMACSHA512AndAES_256", + "jasypt.secret", "${jasypt::ENC(wqp8zDeiCQ5JaFvwDtoAcr2WMLdlD0rjwvo8Rh0thG5qyTQVGxwJjBIiW26y0dtU)}"); + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .addDiscoveredSecretKeysHandlers() + .withSources(new PropertiesConfigSource(properties, "", 0)) + .build(); + + assertEquals("decoded", config.getRawValue("aes-gcm-nopadding.secret")); + assertEquals("12345678", config.getRawValue("jasypt.secret")); + } +} diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/source/MultipleProfilePropertiesConfigSourceTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/source/MultipleProfilePropertiesConfigSourceTest.java index 78ddf3da7..4a8047ef1 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/source/MultipleProfilePropertiesConfigSourceTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/source/MultipleProfilePropertiesConfigSourceTest.java @@ -2,7 +2,7 @@ import static org.testng.Assert.assertEquals; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.Config; import org.jboss.arquillian.container.test.api.Deployment; diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/source/MultiplePropertiesConfigSourceTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/source/MultiplePropertiesConfigSourceTest.java index 80a9e49fd..baf13e782 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/source/MultiplePropertiesConfigSourceTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/source/MultiplePropertiesConfigSourceTest.java @@ -2,7 +2,7 @@ import static org.testng.Assert.assertEquals; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.Config; import org.jboss.arquillian.container.test.api.Deployment; diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/source/OrdinalSourceTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/source/OrdinalSourceTest.java index 55bb94986..a73981c8b 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/source/OrdinalSourceTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/source/OrdinalSourceTest.java @@ -2,7 +2,7 @@ import static org.testng.Assert.assertEquals; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigSource; diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/source/SmallRyeConfigTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/source/SmallRyeConfigTest.java index fb055580f..a074824d1 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/source/SmallRyeConfigTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/source/SmallRyeConfigTest.java @@ -7,7 +7,7 @@ import java.util.List; import java.util.stream.StreamSupport; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigSource; diff --git a/testsuite/extra/src/test/java/io/smallrye/config/test/source/YamlPropertiesTest.java b/testsuite/extra/src/test/java/io/smallrye/config/test/source/YamlPropertiesTest.java index a76cb262f..81729019c 100644 --- a/testsuite/extra/src/test/java/io/smallrye/config/test/source/YamlPropertiesTest.java +++ b/testsuite/extra/src/test/java/io/smallrye/config/test/source/YamlPropertiesTest.java @@ -2,7 +2,7 @@ import static org.testng.Assert.assertEquals; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.Config; import org.jboss.arquillian.container.test.api.Deployment; diff --git a/testsuite/extra/src/test/resources/beans.xml b/testsuite/extra/src/test/resources/beans.xml new file mode 100644 index 000000000..1e2becd94 --- /dev/null +++ b/testsuite/extra/src/test/resources/beans.xml @@ -0,0 +1,5 @@ + + diff --git a/testsuite/pom.xml b/testsuite/pom.xml index 4ee054ddd..53cf656e9 100644 --- a/testsuite/pom.xml +++ b/testsuite/pom.xml @@ -22,13 +22,13 @@ io.smallrye.config smallrye-config-parent - 2.11.1 + 3.7.1 smallrye-config-testsuite pom - SmallRye: MicroProfile Config Test Suite + SmallRye Config: Test Suite extra @@ -53,4 +53,16 @@ + + + + + io.smallrye.testing + smallrye-testing-bom-tck + ${version.smallrye.testing} + import + pom + + + diff --git a/testsuite/tck/pom.xml b/testsuite/tck/pom.xml index 45239c6fa..62c3a2d05 100644 --- a/testsuite/tck/pom.xml +++ b/testsuite/tck/pom.xml @@ -20,12 +20,16 @@ io.smallrye.config smallrye-config-testsuite - 2.11.1 + 3.7.1 smallrye-config-tck - SmallRye: MicroProfile Config TCK + SmallRye Config: MicroProfile Config TCK + + + 4.0.2 + @@ -33,9 +37,6 @@ org.apache.maven.plugins maven-surefire-plugin - - src/test/resources/tck-suite.xml - dummy 45 @@ -45,17 +46,52 @@ 45 Bob - + dummy Tennis 120 - + + + org.eclipse.microprofile.config:microprofile-config-tck + + + + org.eclipse.microprofile.config.tck.ConverterTest + org.eclipse.microprofile.config.tck.ConfigPropertiesTest + org.eclipse.microprofile.config.tck.broken.ConfigPropertiesMissingPropertyInjectionTest + + + + default-test + + test + + + + org.apache.openwebbeans:openwebbeans-spi + org.apache.openwebbeans:openwebbeans-impl + org.apache.openwebbeans.arquillian:owb-arquillian-standalone + + + + + owb + + test + + + + org.jboss.weld:weld-core-impl + org.jboss.arquillian.container:arquillian-weld-embedded + + + + org.apache.maven.plugins maven-surefire-report-plugin - 3.0.0-M7 SmallRye Config tck-results @@ -82,6 +118,14 @@ + + org.eclipse.microprofile.config + microprofile-config-tck + ${version.eclipse.microprofile.config} + test + + + org.jboss.weld weld-core-impl @@ -92,12 +136,27 @@ arquillian-weld-embedded test + + - org.eclipse.microprofile.config - microprofile-config-tck - ${version.eclipse.microprofile.config} + org.apache.openwebbeans + openwebbeans-spi + ${dependency.version.openwebbeans} + test + + + org.apache.openwebbeans + openwebbeans-impl + ${dependency.version.openwebbeans} test + + org.apache.openwebbeans.arquillian + owb-arquillian-standalone + ${dependency.version.openwebbeans} + test + + diff --git a/testsuite/tck/src/test/resources/tck-suite.xml b/testsuite/tck/src/test/resources/tck-suite.xml deleted file mode 100644 index 3b87cfd9b..000000000 --- a/testsuite/tck/src/test/resources/tck-suite.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/to-jakarta.sh b/to-jakarta.sh deleted file mode 100644 index c5ad73b77..000000000 --- a/to-jakarta.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -# move to jakarta parent -find . -type f -name 'pom.xml' -exec sed -i '' 's/smallrye-parent/smallrye-jakarta-parent/g' {} + -# java sources -find . -type f -name '*.java' -exec sed -i '' 's/javax./jakarta./g' {} + -# service loader files -find . -path "*/src/main/resources/META-INF/services/javax*" | sed -e 'p;s/javax/jakarta/g' | xargs -n2 git mv -# docs -find documentation -type f -name '*.md' -exec sed -i '' 's/javax./jakarta./g' {} + - -mvn build-helper:parse-version versions:set -DnewVersion=\${parsedVersion.nextMajorVersion}.0.0-SNAPSHOT -find examples -depth 1 -type d | xargs -I{} mvn -pl {} build-helper:parse-version versions:set -DnewVersion=\${parsedVersion.nextMajorVersion}.0.0-SNAPSHOT - -mvn versions:update-property -Dproperty=version.eclipse.microprofile.config -DnewVersion=3.0 -#https://issues.sonatype.org/browse/MVNCENTRAL-6872 -#mvn versions:update-property -Dproperty=version.jakarta.validation -DnewVersion=3.0 -sed -i '' 's/validation>2.0.23.0.1 io.smallrye.config smallrye-config-parent - 2.11.1 + 3.7.1 ../../ smallrye-config-source-injection - SmallRye: MicroProfile Config Source Injection + SmallRye Config: CDI ConfigSource Injection diff --git a/utils/cdi-provider/src/main/java/io/smallrye/config/util/injection/ConfigSourceMap.java b/utils/cdi-provider/src/main/java/io/smallrye/config/util/injection/ConfigSourceMap.java index ce3e37a97..82901427e 100644 --- a/utils/cdi-provider/src/main/java/io/smallrye/config/util/injection/ConfigSourceMap.java +++ b/utils/cdi-provider/src/main/java/io/smallrye/config/util/injection/ConfigSourceMap.java @@ -5,11 +5,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import javax.inject.Qualifier; +import jakarta.inject.Qualifier; /** * Mark a map that contains the config sources - * + * * @author Phillip Kruger */ @Qualifier diff --git a/utils/cdi-provider/src/main/java/io/smallrye/config/util/injection/ConfigSourceProvider.java b/utils/cdi-provider/src/main/java/io/smallrye/config/util/injection/ConfigSourceProvider.java index 7de310cfb..a6eee7e02 100644 --- a/utils/cdi-provider/src/main/java/io/smallrye/config/util/injection/ConfigSourceProvider.java +++ b/utils/cdi-provider/src/main/java/io/smallrye/config/util/injection/ConfigSourceProvider.java @@ -8,19 +8,19 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import javax.annotation.PostConstruct; -import javax.enterprise.context.Dependent; -import javax.enterprise.inject.Produces; -import javax.enterprise.inject.spi.InjectionPoint; -import javax.inject.Inject; -import javax.inject.Provider; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.inject.Inject; +import jakarta.inject.Provider; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigSource; /** * Making the Config sources available via CDI - * + * * @author Phillip Kruger */ @Dependent diff --git a/utils/cdi-provider/src/main/java/io/smallrye/config/util/injection/Name.java b/utils/cdi-provider/src/main/java/io/smallrye/config/util/injection/Name.java index 82eb4afa4..58408c7e2 100644 --- a/utils/cdi-provider/src/main/java/io/smallrye/config/util/injection/Name.java +++ b/utils/cdi-provider/src/main/java/io/smallrye/config/util/injection/Name.java @@ -5,12 +5,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import javax.enterprise.util.Nonbinding; -import javax.inject.Qualifier; +import jakarta.enterprise.util.Nonbinding; +import jakarta.inject.Qualifier; /** * The define the name of a config source - * + * * @author Phillip Kruger */ @Qualifier diff --git a/utils/cdi-provider/src/test/java/io/smallrye/config/util/injection/ConfigSourceProviderTest.java b/utils/cdi-provider/src/test/java/io/smallrye/config/util/injection/ConfigSourceProviderTest.java index 8f27defc3..fa4ea567b 100644 --- a/utils/cdi-provider/src/test/java/io/smallrye/config/util/injection/ConfigSourceProviderTest.java +++ b/utils/cdi-provider/src/test/java/io/smallrye/config/util/injection/ConfigSourceProviderTest.java @@ -7,7 +7,7 @@ import java.util.Iterator; import java.util.Map; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigSource; @@ -21,7 +21,7 @@ /** * Testing the injection of a Config source name and the Config Source Map - * + * * @author Phillip Kruger */ @ExtendWith(WeldJunit5Extension.class) diff --git a/utils/crypto/pom.xml b/utils/crypto/pom.xml new file mode 100644 index 000000000..7eda45746 --- /dev/null +++ b/utils/crypto/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + smallrye-config-parent + io.smallrye.config + 3.7.1 + ../../pom.xml + + + smallrye-config-crypto + + SmallRye Config: Crypto + + + + io.smallrye.config + smallrye-config + + + + + org.junit.jupiter + junit-jupiter + + + io.smallrye.testing + smallrye-testing-utilities + + + jakarta.annotation + jakarta.annotation-api + test + + + io.smallrye.config + smallrye-config-source-keystore + ${project.version} + test + + + + diff --git a/utils/crypto/src/main/java/io/smallrye/config/crypto/AESGCMNoPaddingSecretKeysHandler.java b/utils/crypto/src/main/java/io/smallrye/config/crypto/AESGCMNoPaddingSecretKeysHandler.java new file mode 100644 index 000000000..71c8de9a1 --- /dev/null +++ b/utils/crypto/src/main/java/io/smallrye/config/crypto/AESGCMNoPaddingSecretKeysHandler.java @@ -0,0 +1,49 @@ +package io.smallrye.config.crypto; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import io.smallrye.config.SecretKeysHandler; + +public class AESGCMNoPaddingSecretKeysHandler implements SecretKeysHandler { + private final SecretKeySpec encryptionKey; + + public AESGCMNoPaddingSecretKeysHandler(final byte[] encryptionKey) { + try { + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + sha256.update(encryptionKey); + this.encryptionKey = new SecretKeySpec(sha256.digest(), "AES"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public String decode(final String secret) { + try { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + ByteBuffer byteBuffer = ByteBuffer.wrap(Base64.getUrlDecoder().decode(secret.getBytes(UTF_8))); + int ivLength = byteBuffer.get(); + byte[] iv = new byte[ivLength]; + byteBuffer.get(iv); + byte[] encrypted = new byte[byteBuffer.remaining()]; + byteBuffer.get(encrypted); + cipher.init(Cipher.DECRYPT_MODE, encryptionKey, new GCMParameterSpec(128, iv)); + return new String(cipher.doFinal(encrypted), UTF_8); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public String getName() { + return "aes-gcm-nopadding"; + } +} diff --git a/utils/crypto/src/main/java/io/smallrye/config/crypto/AESGCMNoPaddingSecretKeysHandlerFactory.java b/utils/crypto/src/main/java/io/smallrye/config/crypto/AESGCMNoPaddingSecretKeysHandlerFactory.java new file mode 100644 index 000000000..f74071880 --- /dev/null +++ b/utils/crypto/src/main/java/io/smallrye/config/crypto/AESGCMNoPaddingSecretKeysHandlerFactory.java @@ -0,0 +1,50 @@ +package io.smallrye.config.crypto; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.NoSuchElementException; + +import io.smallrye.config.ConfigSourceContext; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.Converters; +import io.smallrye.config.SecretKeysHandler; +import io.smallrye.config.SecretKeysHandlerFactory; +import io.smallrye.config._private.ConfigMessages; + +public class AESGCMNoPaddingSecretKeysHandlerFactory implements SecretKeysHandlerFactory { + public static final String ENCRYPTION_KEY = "smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key"; + + @Override + public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context) { + return new LazySecretKeysHandler(new SecretKeysHandlerFactory() { + @Override + public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context) { + ConfigValue encryptionKey = context.getValue(ENCRYPTION_KEY); + if (encryptionKey == null || encryptionKey.getValue() == null) { + throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(ENCRYPTION_KEY)); + } + + boolean decode = false; + ConfigValue plain = context.getValue("smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode"); + if (plain != null && plain.getValue() != null) { + decode = Converters.getImplicitConverter(Boolean.class).convert(plain.getValue()); + } + + byte[] encryptionKeyBytes = decode ? Base64.getUrlDecoder().decode(encryptionKey.getValue()) + : encryptionKey.getValue().getBytes(StandardCharsets.UTF_8); + + return new AESGCMNoPaddingSecretKeysHandler(encryptionKeyBytes); + } + + @Override + public String getName() { + return AESGCMNoPaddingSecretKeysHandlerFactory.this.getName(); + } + }); + } + + @Override + public String getName() { + return "aes-gcm-nopadding"; + } +} diff --git a/utils/crypto/src/main/resources/META-INF/services/io.smallrye.config.SecretKeysHandlerFactory b/utils/crypto/src/main/resources/META-INF/services/io.smallrye.config.SecretKeysHandlerFactory new file mode 100644 index 000000000..3cf6c8d38 --- /dev/null +++ b/utils/crypto/src/main/resources/META-INF/services/io.smallrye.config.SecretKeysHandlerFactory @@ -0,0 +1 @@ +io.smallrye.config.crypto.AESGCMNoPaddingSecretKeysHandlerFactory diff --git a/utils/crypto/src/test/java/io/smallrye/config/crypto/AESGCMNoPaddingSecretKeysHandlerTest.java b/utils/crypto/src/test/java/io/smallrye/config/crypto/AESGCMNoPaddingSecretKeysHandlerTest.java new file mode 100644 index 000000000..12cd889aa --- /dev/null +++ b/utils/crypto/src/test/java/io/smallrye/config/crypto/AESGCMNoPaddingSecretKeysHandlerTest.java @@ -0,0 +1,111 @@ +package io.smallrye.config.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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 org.junit.jupiter.api.Test; + +import io.smallrye.config.ConfigSourceFactory; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; + +class AESGCMNoPaddingSecretKeysHandlerTest { + @Test + void handler() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .addDiscoveredSecretKeysHandlers() + .withDefaultValues(Map.of( + "smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key", + "c29tZWFyYml0cmFyeWNyYXp5c3RyaW5ndGhhdGRvZXNub3RtYXR0ZXI", + "smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode", "true", + "my.secret", "${aes-gcm-nopadding::DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa_kTliQBpuzTobDZxlg}", + "my.expression", "${not.found:default}", + "another.expression", "${my.expression}")) + .build(); + + assertEquals("decoded", config.getRawValue("my.secret")); + assertEquals("default", config.getRawValue("my.expression")); + assertEquals("default", config.getRawValue("another.expression")); + } + + @Test + void plainKey() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .addDiscoveredSecretKeysHandlers() + .withDefaultValues(Map.of( + "smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key", + "somearbitrarycrazystringthatdoesnotmatter", + "my.secret", "${aes-gcm-nopadding::DPZqAC4GZNAXi6_43A4O2SBmaQssGkq6PS7rz8tzHDt1}")) + .build(); + + assertEquals("1234", config.getRawValue("my.secret")); + } + + @Test + void keystore() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .addDiscoveredSources() + .addDiscoveredSecretKeysHandlers() + .withDefaultValues(Map.of( + "smallrye.config.source.keystore.\"properties\".path", "properties", + "smallrye.config.source.keystore.\"properties\".password", "arealpassword", + "smallrye.config.source.keystore.\"properties\".handler", "aes-gcm-nopadding", + "smallrye.config.source.keystore.\"key\".path", "key", + "smallrye.config.source.keystore.\"key\".password", "anotherpassword")) + .build(); + + ConfigValue secret = config.getConfigValue("my.secret"); + assertEquals("1234", secret.getValue()); + } + + @Test + void noEncriptionKey() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .addDiscoveredSecretKeysHandlers() + .withDefaultValues(Map.of( + "my.secret", "${aes-gcm-nopadding::DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa_kTliQBpuzTobDZxlg}")) + .build(); + + assertThrows(NoSuchElementException.class, () -> config.getConfigValue("my.secret")); + + Map properties = Map.of("smallrye.config.secret-handlers", "none"); + new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .addDiscoveredSecretKeysHandlers() + .withSources(new PropertiesConfigSource(properties, "", 0)) + .build(); + assertTrue(true); + } + + @Test + void configurableSource() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .addDiscoveredSources() + .addDiscoveredSecretKeysHandlers() + .withDefaultValues(Map.of( + "smallrye.config.source.keystore.test.path", "keystore", + "smallrye.config.source.keystore.test.password", "secret", + "smallrye.config.source.keystore.test.handler", "aes-gcm-nopadding")) + .withSources((ConfigSourceFactory) context -> List.of( + new PropertiesConfigSource(Map.of( + "smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key", + "c29tZWFyYml0cmFyeWNyYXp5c3RyaW5ndGhhdGRvZXNub3RtYXR0ZXI", + "smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode", "true"), "", 0))) + .build(); + + ConfigValue secret = config.getConfigValue("my.secret"); + assertEquals("decoded", secret.getValue()); + } +} diff --git a/utils/crypto/src/test/resources/key b/utils/crypto/src/test/resources/key new file mode 100644 index 000000000..cfa938083 Binary files /dev/null and b/utils/crypto/src/test/resources/key differ diff --git a/utils/crypto/src/test/resources/keystore b/utils/crypto/src/test/resources/keystore new file mode 100644 index 000000000..74db4bb9a Binary files /dev/null and b/utils/crypto/src/test/resources/keystore differ diff --git a/utils/crypto/src/test/resources/properties b/utils/crypto/src/test/resources/properties new file mode 100644 index 000000000..679464277 Binary files /dev/null and b/utils/crypto/src/test/resources/properties differ diff --git a/utils/events/pom.xml b/utils/events/pom.xml index be9a43dc2..81e349774 100644 --- a/utils/events/pom.xml +++ b/utils/events/pom.xml @@ -5,13 +5,13 @@ io.smallrye.config smallrye-config-parent - 2.11.1 + 3.7.1 ../../ smallrye-config-events - SmallRye: MicroProfile Config Events + SmallRye Config: CDI Events diff --git a/utils/events/src/main/java/io/smallrye/config/events/ChangeEvent.java b/utils/events/src/main/java/io/smallrye/config/events/ChangeEvent.java index 895b70f9d..cdba1ae2b 100644 --- a/utils/events/src/main/java/io/smallrye/config/events/ChangeEvent.java +++ b/utils/events/src/main/java/io/smallrye/config/events/ChangeEvent.java @@ -5,7 +5,7 @@ /** * an Event on a config element - * + * * @author Phillip Kruger */ public class ChangeEvent implements Serializable { diff --git a/utils/events/src/main/java/io/smallrye/config/events/ChangeEventNotifier.java b/utils/events/src/main/java/io/smallrye/config/events/ChangeEventNotifier.java index e5fd2d647..5f3faa108 100644 --- a/utils/events/src/main/java/io/smallrye/config/events/ChangeEventNotifier.java +++ b/utils/events/src/main/java/io/smallrye/config/events/ChangeEventNotifier.java @@ -7,17 +7,17 @@ import java.util.Optional; import java.util.Set; -import javax.enterprise.context.ApplicationScoped; -import javax.enterprise.context.Initialized; -import javax.enterprise.event.Event; -import javax.enterprise.event.Observes; -import javax.inject.Inject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Initialized; +import jakarta.enterprise.event.Event; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; /** * Easy way to fire a change event - * + * * @author Phillip Kruger - * + * * This gets used from Config sources that is not in the CDI Context. So we can not @Inject a bean. * For some reason, CDI.current() is only working on Payara, and not on Thorntail and OpenLiberty, so this ugly footwork * is to diff --git a/utils/events/src/main/java/io/smallrye/config/events/KeyFilter.java b/utils/events/src/main/java/io/smallrye/config/events/KeyFilter.java index 94f9c4153..6c4d554ab 100644 --- a/utils/events/src/main/java/io/smallrye/config/events/KeyFilter.java +++ b/utils/events/src/main/java/io/smallrye/config/events/KeyFilter.java @@ -6,12 +6,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import javax.enterprise.util.AnnotationLiteral; -import javax.inject.Qualifier; +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; /** * Filter the event on the key - * + * * @author Phillip Kruger */ @Qualifier diff --git a/utils/events/src/main/java/io/smallrye/config/events/SourceFilter.java b/utils/events/src/main/java/io/smallrye/config/events/SourceFilter.java index dcbf7bd36..f854111de 100644 --- a/utils/events/src/main/java/io/smallrye/config/events/SourceFilter.java +++ b/utils/events/src/main/java/io/smallrye/config/events/SourceFilter.java @@ -6,12 +6,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import javax.enterprise.util.AnnotationLiteral; -import javax.inject.Qualifier; +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; /** * Filter by a config source - * + * * @author Phillip Kruger */ @Qualifier diff --git a/utils/events/src/main/java/io/smallrye/config/events/TypeFilter.java b/utils/events/src/main/java/io/smallrye/config/events/TypeFilter.java index 1efb4e85c..b6ca942fa 100644 --- a/utils/events/src/main/java/io/smallrye/config/events/TypeFilter.java +++ b/utils/events/src/main/java/io/smallrye/config/events/TypeFilter.java @@ -6,12 +6,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import javax.enterprise.util.AnnotationLiteral; -import javax.inject.Qualifier; +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; /** * filter by change type - * + * * @author Phillip Kruger */ @Qualifier diff --git a/utils/events/src/main/java/io/smallrye/config/events/regex/Field.java b/utils/events/src/main/java/io/smallrye/config/events/regex/Field.java index 3de4fd11f..1fc1965c7 100644 --- a/utils/events/src/main/java/io/smallrye/config/events/regex/Field.java +++ b/utils/events/src/main/java/io/smallrye/config/events/regex/Field.java @@ -2,7 +2,7 @@ /** * a field to apply a regex on - * + * * @author Phillip Kruger */ public enum Field { diff --git a/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilter.java b/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilter.java index e1b6c8064..60cda2222 100644 --- a/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilter.java +++ b/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilter.java @@ -6,12 +6,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import javax.enterprise.util.Nonbinding; -import javax.interceptor.InterceptorBinding; +import jakarta.enterprise.util.Nonbinding; +import jakarta.interceptor.InterceptorBinding; /** * an interceptor that match the value to a regular expression - * + * * @author Phillip Kruger */ @Inherited diff --git a/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilterInterceptor.java b/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilterInterceptor.java index 66f50add3..a0e82f3de 100644 --- a/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilterInterceptor.java +++ b/utils/events/src/main/java/io/smallrye/config/events/regex/RegexFilterInterceptor.java @@ -4,10 +4,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.annotation.Priority; -import javax.interceptor.AroundInvoke; -import javax.interceptor.Interceptor; -import javax.interceptor.InvocationContext; +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; import io.smallrye.config.events.ChangeEvent; diff --git a/utils/events/src/test/java/io/smallrye/config/events/ChangeEventNotifierTest.java b/utils/events/src/test/java/io/smallrye/config/events/ChangeEventNotifierTest.java index bb97ee558..c3d805508 100644 --- a/utils/events/src/test/java/io/smallrye/config/events/ChangeEventNotifierTest.java +++ b/utils/events/src/test/java/io/smallrye/config/events/ChangeEventNotifierTest.java @@ -2,8 +2,8 @@ import java.util.Optional; -import javax.enterprise.context.ApplicationScoped; -import javax.enterprise.event.Observes; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; import org.jboss.weld.junit5.WeldInitiator; import org.jboss.weld.junit5.WeldJunit5Extension; @@ -17,7 +17,7 @@ /** * Testing that the events fire correctly - * + * * @author Phillip Kruger */ @ExtendWith(WeldJunit5Extension.class) diff --git a/utils/jasypt/pom.xml b/utils/jasypt/pom.xml new file mode 100644 index 000000000..fde170688 --- /dev/null +++ b/utils/jasypt/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + smallrye-config-parent + io.smallrye.config + 3.7.1 + ../../pom.xml + + + smallrye-config-jasypt + + SmallRye Config: Jasypt + + + 1.9.3 + + + + + io.smallrye.config + smallrye-config + + + org.jasypt + jasypt + ${version.jasypt} + + + + + org.junit.jupiter + junit-jupiter + + + io.smallrye.testing + smallrye-testing-utilities + + + jakarta.annotation + jakarta.annotation-api + test + + + + diff --git a/utils/jasypt/src/main/java/io/smallrye/config/jasypt/JasyptSecretKeysHandler.java b/utils/jasypt/src/main/java/io/smallrye/config/jasypt/JasyptSecretKeysHandler.java new file mode 100644 index 000000000..7cb499e1e --- /dev/null +++ b/utils/jasypt/src/main/java/io/smallrye/config/jasypt/JasyptSecretKeysHandler.java @@ -0,0 +1,29 @@ +package io.smallrye.config.jasypt; + +import org.jasypt.encryption.pbe.StandardPBEStringEncryptor; +import org.jasypt.iv.RandomIvGenerator; +import org.jasypt.properties.PropertyValueEncryptionUtils; + +import io.smallrye.config.SecretKeysHandler; + +public class JasyptSecretKeysHandler implements SecretKeysHandler { + private final StandardPBEStringEncryptor encryptor; + + public JasyptSecretKeysHandler(final String password, final String algorithm) { + encryptor = new StandardPBEStringEncryptor(); + encryptor.setPassword(password); + encryptor.setAlgorithm(algorithm); + encryptor.setIvGenerator(new RandomIvGenerator()); + encryptor.initialize(); + } + + @Override + public String decode(final String secret) { + return PropertyValueEncryptionUtils.decrypt(secret, encryptor); + } + + @Override + public String getName() { + return "jasypt"; + } +} diff --git a/utils/jasypt/src/main/java/io/smallrye/config/jasypt/JasyptSecretKeysHandlerFactory.java b/utils/jasypt/src/main/java/io/smallrye/config/jasypt/JasyptSecretKeysHandlerFactory.java new file mode 100644 index 000000000..6796155d0 --- /dev/null +++ b/utils/jasypt/src/main/java/io/smallrye/config/jasypt/JasyptSecretKeysHandlerFactory.java @@ -0,0 +1,31 @@ +package io.smallrye.config.jasypt; + +import java.util.NoSuchElementException; + +import io.smallrye.config.ConfigSourceContext; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.SecretKeysHandler; +import io.smallrye.config.SecretKeysHandlerFactory; +import io.smallrye.config._private.ConfigMessages; + +public class JasyptSecretKeysHandlerFactory implements SecretKeysHandlerFactory { + @Override + public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context) { + String password = requireValue(context, "smallrye.config.secret-handler.jasypt.password"); + String algorithm = requireValue(context, "smallrye.config.secret-handler.jasypt.algorithm"); + return new JasyptSecretKeysHandler(password, algorithm); + } + + @Override + public String getName() { + return "jasypt"; + } + + private static String requireValue(final ConfigSourceContext context, final String name) { + ConfigValue value = context.getValue(name); + if (value != null) { + return value.getValue(); + } + throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(name)); + } +} diff --git a/utils/jasypt/src/main/resources/META-INF/services/io.smallrye.config.SecretKeysHandlerFactory b/utils/jasypt/src/main/resources/META-INF/services/io.smallrye.config.SecretKeysHandlerFactory new file mode 100644 index 000000000..bb867b0b5 --- /dev/null +++ b/utils/jasypt/src/main/resources/META-INF/services/io.smallrye.config.SecretKeysHandlerFactory @@ -0,0 +1 @@ +io.smallrye.config.jasypt.JasyptSecretKeysHandlerFactory diff --git a/utils/jasypt/src/test/java/io/smallrye/config/jasypt/JasyptSecretKeysHandlerTest.java b/utils/jasypt/src/test/java/io/smallrye/config/jasypt/JasyptSecretKeysHandlerTest.java new file mode 100644 index 000000000..b2854d8fd --- /dev/null +++ b/utils/jasypt/src/test/java/io/smallrye/config/jasypt/JasyptSecretKeysHandlerTest.java @@ -0,0 +1,29 @@ +package io.smallrye.config.jasypt; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; + +class JasyptSecretKeysHandlerTest { + @Test + void jasypt() { + Map properties = Map.of( + "smallrye.config.secret-handler.jasypt.password", "jasypt", + "smallrye.config.secret-handler.jasypt.algorithm", "PBEWithHMACSHA512AndAES_256", + "my.secret", "${jasypt::ENC(wqp8zDeiCQ5JaFvwDtoAcr2WMLdlD0rjwvo8Rh0thG5qyTQVGxwJjBIiW26y0dtU)}"); + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultInterceptors() + .addDiscoveredSecretKeysHandlers() + .withSources(new PropertiesConfigSource(properties, "", 0)) + .build(); + + assertEquals("12345678", config.getRawValue("my.secret")); + } +} diff --git a/validator/pom.xml b/validator/pom.xml index 80a09cf35..9996b0662 100644 --- a/validator/pom.xml +++ b/validator/pom.xml @@ -20,19 +20,19 @@ io.smallrye.config smallrye-config-parent - 2.11.1 + 3.7.1 smallrye-config-validator - SmallRye: MicroProfile Config Validator + SmallRye Config: Validator - 2.0.2 + 3.0.1 - 6.2.3.Final - 3.0.4 + 8.0.0.Final + 5.0.0 @@ -59,10 +59,22 @@ test - org.glassfish - jakarta.el - ${version.jakarta.el} + org.glassfish.expressly + expressly + ${version.glassfish.expressly} test + + + + + org.apache.maven.plugins + maven-surefire-plugin + + -Duser.language=en -Duser.region=US + + + + diff --git a/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidator.java b/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidator.java index 943a58993..836f75503 100644 --- a/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidator.java +++ b/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidator.java @@ -1,6 +1,7 @@ package io.smallrye.config.validator; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.UndeclaredThrowableException; import java.util.ArrayList; import java.util.Collection; @@ -9,14 +10,14 @@ import java.util.Optional; import java.util.Set; -import javax.validation.ConstraintViolation; -import javax.validation.Path; -import javax.validation.Validator; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Path; +import jakarta.validation.Validator; +import io.smallrye.config.ConfigMapping.NamingStrategy; import io.smallrye.config.ConfigMappingInterface; import io.smallrye.config.ConfigMappingInterface.CollectionProperty; import io.smallrye.config.ConfigMappingInterface.MapProperty; -import io.smallrye.config.ConfigMappingInterface.NamingStrategy; import io.smallrye.config.ConfigMappingInterface.Property; import io.smallrye.config.ConfigValidationException; import io.smallrye.config.ConfigValidationException.Problem; @@ -38,7 +39,7 @@ default void validateMapping( if (mappingInterface != null) { validateMappingInterface(mappingInterface, prefix, mappingInterface.getNamingStrategy(), mappingObject, problems); } else { - validateMappingClass(mappingObject, problems); + validateMappingClass(mappingObject, problems, prefix); } if (!problems.isEmpty()) { @@ -53,11 +54,17 @@ default void validateMappingInterface( final Object mappingObject, final List problems) { + for (ConfigMappingInterface superType : mappingInterface.getSuperTypes()) { + for (Property property : superType.getProperties()) { + validateProperty(property, currentPath, namingStrategy, mappingObject, false, problems); + } + } + for (Property property : mappingInterface.getProperties()) { validateProperty(property, currentPath, namingStrategy, mappingObject, false, problems); } - validateMappingClass(mappingObject, problems); + validateMappingClass(mappingObject, problems, currentPath); } default void validateProperty( @@ -83,12 +90,13 @@ default void validateProperty( // unwrap if (optional) { Optional optionalGroup = (Optional) group; - if (!optionalGroup.isPresent()) { + if (optionalGroup.isEmpty()) { return; } group = optionalGroup.get(); } + validatePropertyValue(property, currentPath, namingStrategy, mappingObject, problems); validateMappingInterface(property.asGroup().getGroupType(), appendPropertyName(currentPath, property), namingStrategy, group, problems); } catch (IllegalAccessException e) { @@ -108,7 +116,15 @@ default void validateProperty( CollectionProperty collectionProperty = property.asCollection(); if (collectionProperty.getElement().isGroup()) { try { - Collection collection = (Collection) property.getMethod().invoke(mappingObject); + Object object = property.getMethod().invoke(mappingObject); + if (optional) { + Optional optionalCollection = (Optional) object; + if (optionalCollection.isEmpty()) { + return; + } + object = optionalCollection.get(); + } + Collection collection = (Collection) object; int i = 0; for (Object element : collection) { validateMappingInterface(collectionProperty.getElement().asGroup().getGroupType(), @@ -127,10 +143,8 @@ default void validateProperty( throw new UndeclaredThrowableException(t2); } } - } else if (collectionProperty.getElement().isLeaf()) { - validateProperty(collectionProperty.getElement(), currentPath, namingStrategy, mappingObject, optional, - problems); } + validatePropertyValue(property, currentPath, namingStrategy, mappingObject, problems); } if (property.isMap()) { @@ -169,8 +183,6 @@ default void validateProperty( i++; } } - } else if (collectionProperty.getElement().isLeaf()) { - validatePropertyValue(property, currentPath, namingStrategy, mappingObject, problems); } } catch (IllegalAccessException e) { throw new IllegalAccessError(e.getMessage()); @@ -183,9 +195,8 @@ default void validateProperty( throw new UndeclaredThrowableException(t2); } } - } else if (mapProperty.getValueProperty().isLeaf()) { - validatePropertyValue(property, currentPath, namingStrategy, mappingObject, problems); } + validatePropertyValue(property, currentPath, namingStrategy, mappingObject, problems); } } @@ -197,9 +208,20 @@ default void validatePropertyValue( final List problems) { try { + Method methodToInvoke; + if (property.getMethod().canAccess(mappingObject)) { + methodToInvoke = property.getMethod(); + } else { + try { + methodToInvoke = mappingObject.getClass().getMethod(property.getMethod().getName()); + } catch (NoSuchMethodException e) { + // This never happens, because we generated the class, and we know the method exists + throw new RuntimeException(e); + } + } + Set> violations = getValidator().forExecutables().validateReturnValue(mappingObject, - property.getMethod(), - property.getMethod().invoke(mappingObject)); + property.getMethod(), methodToInvoke.invoke(mappingObject)); for (ConstraintViolation violation : violations) { problems.add(new Problem(interpolateMessage(currentPath, namingStrategy, property, violation))); } @@ -216,11 +238,15 @@ default void validatePropertyValue( } } - default void validateMappingClass(final Object mappingObject, final List problems) { + default void validateMappingClass( + final Object mappingObject, + final List problems, + final String currentPath) { final Set> violations = getValidator().validate(mappingObject); for (ConstraintViolation violation : violations) { - problems.add(violation.getPropertyPath().toString().isEmpty() ? new Problem(violation.getMessage()) - : new Problem(violation.getPropertyPath() + " " + violation.getMessage())); + problems.add( + violation.getPropertyPath().toString().isEmpty() ? new Problem(currentPath + " " + violation.getMessage()) + : new Problem(currentPath + " " + violation.getPropertyPath() + " " + violation.getMessage())); } } diff --git a/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidatorImpl.java b/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidatorImpl.java index 54aa1ac8a..868a5837e 100644 --- a/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidatorImpl.java +++ b/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidatorImpl.java @@ -1,7 +1,7 @@ package io.smallrye.config.validator; -import javax.validation.Validation; -import javax.validation.Validator; +import jakarta.validation.Validation; +import jakarta.validation.Validator; public class BeanValidationConfigValidatorImpl implements BeanValidationConfigValidator { private Validator validator; diff --git a/validator/src/test/java/io/smallrye/config/validator/ValidateConfigTest.java b/validator/src/test/java/io/smallrye/config/validator/ValidateConfigTest.java index bef4bf9c7..30b07ec5a 100644 --- a/validator/src/test/java/io/smallrye/config/validator/ValidateConfigTest.java +++ b/validator/src/test/java/io/smallrye/config/validator/ValidateConfigTest.java @@ -2,38 +2,69 @@ import static io.smallrye.config.validator.KeyValuesConfigSource.config; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.OptionalInt; - -import javax.validation.Constraint; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import javax.validation.Payload; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import javax.validation.constraints.Size; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jakarta.validation.Constraint; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.Payload; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; import org.eclipse.microprofile.config.inject.ConfigProperties; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import io.smallrye.config.ConfigMapping; import io.smallrye.config.ConfigValidationException; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.WithDefault; import io.smallrye.config.WithParentName; public class ValidateConfigTest { + + private static Locale oldDefaultLocale = Locale.getDefault(); + + /** + * Set the default locale to ROOT before the tests as the validation problem messages are locale-sensitive. + */ + @BeforeAll + static void setupMessageLocale() { + Locale.setDefault(Locale.ROOT); + } + + /** + * Restore the old default locale just in case it is needed elsewhere outside this test class. + */ + @AfterAll + static void restoreMessageLocale() { + Locale.setDefault(oldDefaultLocale); + } + @Test void validateConfigMapping() { SmallRyeConfig config = new SmallRyeConfigBuilder() @@ -42,15 +73,22 @@ void validateConfigMapping() { "server.host", "localhost", "server.port", "8080", "server.log.days", "20", + "server.log.levels.INFO.importance", "1", + "server.log.levels.INFO.description", "just info", + "server.log.levels.ERROR.importance", "-1", + "server.log.levels.ERROR.description", "serious error", "server.proxy.enable", "true", "server.proxy.timeout", "20", "server.form.login-page", "login.html", "server.form.error-page", "error.html", "server.form.landing-page", "index.html", + "server.form.x", "xyz", "server.cors.origins[0].host", "some-server", "server.cors.origins[0].port", "9000", "server.cors.origins[1].host", "localhost", "server.cors.origins[1].port", "1", + "server.cors.origins[2].host", "server3", + "server.cors.origins[2].port", "4", "server.cors.methods[0]", "GET", "server.cors.methods[1]", "POST", "server.info.name", "Bond", @@ -69,22 +107,29 @@ void validateConfigMapping() { for (int i = 0; i < validationException.getProblemCount(); i++) { validations.add(validationException.getProblem(i).getMessage()); } - assertEquals(15, validations.size()); - assertTrue(validations.contains("server.port must be less than or equal to 10")); - assertTrue(validations.contains("server.log.days must be less than or equal to 15")); - assertTrue(validations.contains("server.proxy.timeout must be less than or equal to 10")); - assertTrue(validations.contains("server.cors.origins[0].host size must be between 0 and 10")); - assertTrue(validations.contains("server.cors.origins[0].port must be less than or equal to 10")); - assertTrue(validations.contains("server.cors.methods[1] size must be between 0 and 3")); - assertTrue(validations.contains("server.form.login-page size must be between 0 and 3")); - assertTrue(validations.contains("server.form.error-page size must be between 0 and 3")); - assertTrue(validations.contains("server.form.landing-page size must be between 0 and 3")); - assertTrue(validations.contains("server.info.name size must be between 0 and 3")); - assertTrue(validations.contains("server.info.code must be less than or equal to 3")); - assertTrue(validations.contains("server.info.alias[0] size must be between 0 and 3")); - assertTrue(validations.contains("server.info.admins.root[1].username size must be between 0 and 4")); - assertTrue(validations.contains("server.info.firewall.accepted[1] size must be between 8 and 15")); - assertTrue(validations.contains("server is not prod")); + assertValidationsEqual(validations, + "server.port must be less than or equal to 10", + "server.log.days must be less than or equal to 15", + "server.log.levels all identifiers must have the same length", + "server.log.levels.ERROR.importance must be greater than or equal to 0", + "server.proxy.timeout must be less than or equal to 10", + "server.cors.origins size must be between 4 and 2147483647", + "server.cors.origins[0].host size must be between 0 and 10", + "server.cors.origins[0].port must be less than or equal to 10", + "server.cors.origins[2] someClassLevelCrossValidation If host is server3, then port value must be 3", + "server.cors.methods[1] size must be between 0 and 3", + "server.cors.methods size must be between 3 and 2147483647", + "server.form.login-page size must be between 0 and 3", + "server.form.error-page size must be between 0 and 3", + "server.form.landing-page size must be between 0 and 3", + "server.form.x size must be between 2 and 2147483647", + "server.info.name size must be between 0 and 3", + "server.info.code must be less than or equal to 3", + "server.info.alias[0] size must be between 0 and 3", + "server.info.admins.root[1].username size must be between 0 and 4", + "server.info.admins.root size must be between 0 and 1", + "server.info.firewall.accepted[1] size must be between 8 and 15", + "server server is not prod"); } @Test @@ -119,7 +164,7 @@ void validateConfigProperties() { assertEquals(1, validationException.getProblemCount()); List validations = new ArrayList<>(); validations.add(validationException.getProblem(0).getMessage()); - assertTrue(validations.contains("port must be less than or equal to 10")); + assertTrue(validations.contains("client port must be less than or equal to 10")); } @Test @@ -145,7 +190,7 @@ public interface Server { @Max(10) int port(); - Map form(); + Map<@Size(min = 2) String, @Size(max = 3) String> form(); Optional proxy(); @@ -165,11 +210,25 @@ interface Proxy { interface Log { @Max(15) int days(); + + @EqualLengthKeys + Map levels(); + + interface LogLevel { + + @PositiveOrZero + int importance(); + + @NotBlank + String description(); + } } interface Cors { + @Size(min = 4) List origins(); + @Size(min = 3) List<@Size(max = 3) String> methods(); interface Origin { @@ -178,6 +237,11 @@ interface Origin { @Max(10) int port(); + + @AssertTrue(message = "If host is server3, then port value must be 3") + private boolean isSomeClassLevelCrossValidation() { + return !"server3".equals(host()) || port() == 3; + } } } @@ -189,7 +253,7 @@ interface Info { Optional> alias(); - Map> admins(); + Map> admins(); Map> firewall(); @@ -218,6 +282,24 @@ public boolean isValid(final Server value, final ConstraintValidatorContext cont } } + @Target({ METHOD, ANNOTATION_TYPE }) + @Retention(RUNTIME) + @Constraint(validatedBy = { EqualLengthKeysValidator.class }) + public @interface EqualLengthKeys { + String message() default "all identifiers must have the same length"; + + Class[] groups() default {}; + + Class[] payload() default {}; + } + + public static class EqualLengthKeysValidator implements ConstraintValidator> { + @Override + public boolean isValid(final Map value, final ConstraintValidatorContext context) { + return value.keySet().stream().mapToInt(String::length).distinct().count() <= 1; + } + } + @ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.SNAKE_CASE) public interface ServerNamingStrategy { String theHost(); @@ -245,4 +327,143 @@ interface Parent { int port(); } } + + @Test + void optionalLists() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withValidator(new BeanValidationConfigValidatorImpl()) + .withMapping(Optionals.class) + .withSources(config( + "optionals.list[0].value", "value", + "optionals.list-map[0].value", "value")) + .build(); + + Optionals mapping = config.getConfigMapping(Optionals.class); + + assertTrue(mapping.list().isPresent()); + assertEquals("value", mapping.list().get().get(0).value()); + assertTrue(mapping.listMap().isPresent()); + assertEquals("value", mapping.listMap().get().get(0).get("value")); + } + + @ConfigMapping(prefix = "optionals") + interface Optionals { + Optional> list(); + + Optional>> listMap(); + + interface Nested { + String value(); + } + } + + @Test + void hierarchy() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withValidator(new BeanValidationConfigValidatorImpl()) + .withMapping(Child.class) + .withSources(config("validator.child.number", "1")) + .build(); + + ConfigValidationException validationException = assertThrows(ConfigValidationException.class, + () -> config.getConfigMapping(Child.class)); + List validations = new ArrayList<>(); + for (int i = 0; i < validationException.getProblemCount(); i++) { + validations.add(validationException.getProblem(i).getMessage()); + } + assertEquals(1, validations.size()); + assertTrue(validations.contains("validator.child.number must be greater than or equal to 10")); + } + + public interface Parent { + @Min(10) + Integer number(); + } + + @ConfigMapping(prefix = "validator.child") + public interface Child extends Parent { + + } + + @Test + void nestedMethodValidation() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withValidator(new BeanValidationConfigValidatorImpl()) + .withMapping(MethodValidation.class) + .build(); + + ConfigValidationException validationException = assertThrows(ConfigValidationException.class, + () -> config.getConfigMapping(MethodValidation.class)); + List validations = new ArrayList<>(); + for (int i = 0; i < validationException.getProblemCount(); i++) { + validations.add(validationException.getProblem(i).getMessage()); + } + + assertEquals(1, validationException.getProblemCount()); + assertTrue(validations.contains("method.validation.nested validation executed")); + } + + @ConfigMapping(prefix = "method.validation") + interface MethodValidation { + @NestedValidation + Nested nested(); + + interface Nested { + @WithDefault("value") + String value(); + } + } + + @Target({ METHOD, TYPE }) + @Retention(RUNTIME) + @Constraint(validatedBy = { NestedValidator.class }) + public @interface NestedValidation { + String message() default "validation executed"; + + Class[] groups() default {}; + + Class[] payload() default {}; + } + + public static class NestedValidator implements ConstraintValidator { + @Override + public boolean isValid(final MethodValidation.Nested value, final ConstraintValidatorContext context) { + return false; + } + } + + private static void assertValidationsEqual(List validations, String... expectedProblemMessages) { + List remainingActual = new ArrayList<>(validations); + List remainingExpected = Stream.of(expectedProblemMessages).collect(Collectors.toList()); + computeListDifference(remainingActual, remainingExpected); + StringBuilder failureMessage = new StringBuilder(); + addFailureToMessage("The following validation problems are missing:", remainingExpected, failureMessage); + addFailureToMessage("The following validation problems were not expected:", remainingActual, failureMessage); + if (failureMessage.length() > 0) { + fail(failureMessage.toString()); + } + } + + private static void computeListDifference(List remainingActual, List remainingExpected) { + // Not using removeAll here as that would erase duplicates, whereas this "Removes the first occurrence [..]" + Iterator remainingExpectedIterator = remainingExpected.iterator(); + while (remainingExpectedIterator.hasNext()) { + boolean expectedFound = remainingActual.remove(remainingExpectedIterator.next()); + if (expectedFound) { + remainingExpectedIterator.remove(); + } + } + } + + private static void addFailureToMessage(String description, List list, StringBuilder failureMessage) { + if (!list.isEmpty()) { + failureMessage.append("\n"); + failureMessage.append(description); + for (String element : list) { + failureMessage.append("\n \""); + failureMessage.append(element); + failureMessage.append("\""); + } + } + } } diff --git a/validator/src/test/java/io/smallrye/config/validator/external/ValidationVisibilityTest.java b/validator/src/test/java/io/smallrye/config/validator/external/ValidationVisibilityTest.java new file mode 100644 index 000000000..9d3b29281 --- /dev/null +++ b/validator/src/test/java/io/smallrye/config/validator/external/ValidationVisibilityTest.java @@ -0,0 +1,32 @@ +package io.smallrye.config.validator.external; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.hibernate.validator.constraints.Length; +import org.junit.jupiter.api.Test; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.validator.BeanValidationConfigValidatorImpl; + +public class ValidationVisibilityTest { + @Test + void visibility() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withDefaultValue("default.visibility.value", "12345678") + .withValidator(new BeanValidationConfigValidatorImpl()) + .withMapping(DefaultVisibility.class) + .build(); + + DefaultVisibility mapping = config.getConfigMapping(DefaultVisibility.class); + + assertEquals("12345678", mapping.value()); + } + + @ConfigMapping(prefix = "default.visibility") + interface DefaultVisibility { + @Length(max = 10) + String value(); + } +}