diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigMappingUtils.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigMappingUtils.java index 6951c24f6aaa2..65a31c9132ad3 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigMappingUtils.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigMappingUtils.java @@ -9,13 +9,13 @@ import java.util.Optional; import java.util.Set; +import org.eclipse.microprofile.config.spi.Converter; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; -import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; import io.quarkus.deployment.annotations.BuildProducer; @@ -26,6 +26,10 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.util.ReflectUtil; import io.smallrye.config.ConfigMapping; +import io.smallrye.config.ConfigMappingInterface; +import io.smallrye.config.ConfigMappingInterface.LeafProperty; +import io.smallrye.config.ConfigMappingInterface.MapProperty; +import io.smallrye.config.ConfigMappingInterface.Property; import io.smallrye.config.ConfigMappingLoader; import io.smallrye.config.ConfigMappingMetadata; @@ -33,12 +37,10 @@ public class ConfigMappingUtils { public static final DotName CONFIG_MAPPING_NAME = DotName.createSimple(ConfigMapping.class.getName()); - private static final DotName OPTIONAL = DotName.createSimple(Optional.class.getName()); - private ConfigMappingUtils() { } - public static void generateConfigClasses( + public static void processConfigClasses( CombinedIndexBuildItem combinedIndex, BuildProducer generatedClasses, BuildProducer reflectiveClasses, @@ -73,7 +75,7 @@ public static void processExtensionConfigMapping( configClasses); } - static void processConfigClass( + private static void processConfigClass( Class configClass, Kind configClassKind, String prefix, @@ -85,45 +87,76 @@ static void processConfigClass( List configMappingsMetadata = ConfigMappingLoader.getConfigMappingsMetadata(configClass); Set generatedClassesNames = new HashSet<>(); - Set mappingsInfo = new HashSet<>(); configMappingsMetadata.forEach(mappingMetadata -> { - generatedClasses.produce( - new GeneratedClassBuildItem(isApplicationClass, mappingMetadata.getClassName(), - mappingMetadata.getClassBytes())); - reflectiveClasses - .produce(ReflectiveClassBuildItem.builder(mappingMetadata.getInterfaceType()).methods().build()); - reflectiveClasses - .produce(ReflectiveClassBuildItem.builder(mappingMetadata.getClassName()) - .methods().build()); - + generatedClassesNames.add(mappingMetadata.getClassName()); + // This is the generated implementation of the mapping by SmallRye Config. + generatedClasses.produce(new GeneratedClassBuildItem(isApplicationClass, mappingMetadata.getClassName(), + mappingMetadata.getClassBytes())); + // Register the interface and implementation methods for reflection. This is required for Bean Validation. + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(mappingMetadata.getInterfaceType()).methods().build()); + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(mappingMetadata.getClassName()).methods().build()); + // Register also the interface hierarchy for (Class parent : getHierarchy(mappingMetadata.getInterfaceType())) { reflectiveClasses.produce(ReflectiveClassBuildItem.builder(parent).methods().build()); } - generatedClassesNames.add(mappingMetadata.getClassName()); + processProperties(mappingMetadata.getInterfaceType(), reflectiveClasses); + }); - ClassInfo mappingInfo = combinedIndex.getIndex() - .getClassByName(DotName.createSimple(mappingMetadata.getInterfaceType().getName())); - if (mappingInfo != null) { - mappingsInfo.add(mappingInfo); + configClasses.produce(new ConfigClassBuildItem(configClass, collectTypes(combinedIndex, configClass), + generatedClassesNames, prefix, configClassKind)); + } + + private static void processProperties( + Class configClass, + BuildProducer reflectiveClasses) { + + ConfigMappingInterface mapping = ConfigMappingLoader.getConfigMapping(configClass); + for (Property property : mapping.getProperties()) { + Class returnType = property.getMethod().getReturnType(); + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(returnType).methods().build()); + + if (property.hasConvertWith()) { + Class> convertWith; + if (property.isLeaf()) { + convertWith = property.asLeaf().getConvertWith(); + } else { + convertWith = property.asPrimitive().getConvertWith(); + } + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(convertWith).build()); } - }); - // For implicit converters - for (ClassInfo classInfo : mappingsInfo) { - for (MethodInfo method : classInfo.methods()) { - Type type = method.returnType(); - if (type.name().equals(OPTIONAL)) { - // E.g. Optional - type = type.asParameterizedType().arguments().get(0); + registerImplicitConverter(property, reflectiveClasses); + + if (property.isMap()) { + MapProperty mapProperty = property.asMap(); + if (mapProperty.hasKeyConvertWith()) { + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(mapProperty.getKeyConvertWith()).build()); + } else { + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(mapProperty.getKeyRawType()).build()); } - reflectiveClasses - .produce(ReflectiveClassBuildItem.builder(type.name().toString()).methods().build()); + + registerImplicitConverter(mapProperty.getValueProperty(), reflectiveClasses); } } + } - configClasses.produce(new ConfigClassBuildItem(configClass, collectTypes(combinedIndex, configClass), - generatedClassesNames, prefix, configClassKind)); + private static void registerImplicitConverter( + Property property, + BuildProducer reflectiveClasses) { + + if (property.isLeaf() && !property.isOptional()) { + LeafProperty leafProperty = property.asLeaf(); + if (leafProperty.hasConvertWith()) { + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(leafProperty.getConvertWith()).build()); + } else { + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(leafProperty.getValueRawType()).methods().build()); + } + } else if (property.isOptional()) { + registerImplicitConverter(property.asOptional().getNestedProperty(), reflectiveClasses); + } else if (property.isCollection()) { + registerImplicitConverter(property.asCollection().getElement(), reflectiveClasses); + } } public static Object newInstance(Class configClass) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java index 749d144ec1b63..29d490c2cc658 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java @@ -1,5 +1,7 @@ package io.quarkus.deployment.steps; +import static io.quarkus.deployment.configuration.ConfigMappingUtils.CONFIG_MAPPING_NAME; +import static io.quarkus.deployment.configuration.ConfigMappingUtils.processConfigClasses; import static io.quarkus.deployment.configuration.ConfigMappingUtils.processExtensionConfigMapping; import static io.quarkus.deployment.steps.ConfigBuildSteps.SERVICES_PREFIX; import static io.quarkus.deployment.util.ServiceUtil.classNamesNamedIn; @@ -204,6 +206,16 @@ void runtimeDefaultsConfig( runTimeConfigBuilder.produce(new RunTimeConfigBuilderBuildItem(builderClassName)); } + @BuildStep + void mappings( + CombinedIndexBuildItem combinedIndex, + BuildProducer generatedClasses, + BuildProducer reflectiveClasses, + BuildProducer configClasses) { + + processConfigClasses(combinedIndex, generatedClasses, reflectiveClasses, configClasses, CONFIG_MAPPING_NAME); + } + @BuildStep void extensionMappings(ConfigurationBuildItem configItem, CombinedIndexBuildItem combinedIndex, diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java index 1835dcec7a1bf..3485fb2959f7b 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java @@ -5,6 +5,7 @@ import static io.quarkus.deployment.builditem.ConfigClassBuildItem.Kind.MAPPING; import static io.quarkus.deployment.builditem.ConfigClassBuildItem.Kind.PROPERTIES; import static io.quarkus.deployment.configuration.ConfigMappingUtils.CONFIG_MAPPING_NAME; +import static io.quarkus.deployment.configuration.ConfigMappingUtils.processConfigClasses; import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; @@ -67,13 +68,11 @@ import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.deployment.configuration.ConfigMappingUtils; import io.quarkus.deployment.configuration.definition.RootDefinition; import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.gizmo.ResultHandle; import io.quarkus.runtime.annotations.ConfigPhase; import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix; -import io.smallrye.config.WithConverter; import io.smallrye.config.inject.ConfigProducer; /** @@ -89,7 +88,6 @@ public class ConfigBuildStep { private static final DotName SR_CONFIG = DotName.createSimple(io.smallrye.config.SmallRyeConfig.class.getName()); private static final DotName SR_CONFIG_VALUE_NAME = DotName.createSimple(io.smallrye.config.ConfigValue.class.getName()); - private static final DotName SR_WITH_CONVERTER = DotName.createSimple(WithConverter.class.getName()); private static final DotName MAP_NAME = DotName.createSimple(Map.class.getName()); private static final DotName SET_NAME = DotName.createSimple(Set.class.getName()); @@ -277,17 +275,13 @@ public void transform(TransformationContext context) { } @BuildStep - void generateConfigClasses( + void generateConfigProperties( CombinedIndexBuildItem combinedIndex, BuildProducer generatedClasses, BuildProducer reflectiveClasses, BuildProducer configClasses) { - // TODO - Generation of Mapping interface classes can be done in core because they don't require CDI - ConfigMappingUtils.generateConfigClasses(combinedIndex, generatedClasses, reflectiveClasses, configClasses, - CONFIG_MAPPING_NAME); - ConfigMappingUtils.generateConfigClasses(combinedIndex, generatedClasses, reflectiveClasses, configClasses, - MP_CONFIG_PROPERTIES_NAME); + processConfigClasses(combinedIndex, generatedClasses, reflectiveClasses, configClasses, MP_CONFIG_PROPERTIES_NAME); } @BuildStep @@ -378,19 +372,6 @@ void registerConfigPropertiesBean( } } - @BuildStep - void registerConfigMappingConverters(CombinedIndexBuildItem indexBuildItem, - BuildProducer producer) { - - String[] valueTypes = indexBuildItem.getIndex().getAnnotations(SR_WITH_CONVERTER).stream() - .map(i -> i.value().asClass().name().toString()) - .toArray(String[]::new); - if (valueTypes.length > 0) { - producer.produce( - ReflectiveClassBuildItem.builder(valueTypes).build()); - } - } - @BuildStep void validateConfigMappingsInjectionPoints( ArcConfig arcConfig, diff --git a/integration-tests/smallrye-config/src/main/java/io/quarkus/it/smallrye/config/ImplicitConverters.java b/integration-tests/smallrye-config/src/main/java/io/quarkus/it/smallrye/config/ImplicitConverters.java new file mode 100644 index 0000000000000..5487b15881e6d --- /dev/null +++ b/integration-tests/smallrye-config/src/main/java/io/quarkus/it/smallrye/config/ImplicitConverters.java @@ -0,0 +1,67 @@ +package io.quarkus.it.smallrye.config; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigMapping(prefix = "implicit.converters") +public interface ImplicitConverters { + @WithDefault("value") + Optional optional(); + + @WithDefault("value") + List list(); + + Map map(); + + class ImplicitOptional { + private final String value; + + public ImplicitOptional(final String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static ImplicitOptional of(String value) { + return new ImplicitOptional("converted"); + } + } + + class ImplicitElement { + private final String value; + + public ImplicitElement(final String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static ImplicitElement of(String value) { + return new ImplicitElement("converted"); + } + } + + class ImplicitValue { + private final String value; + + public ImplicitValue(final String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static ImplicitValue of(String value) { + return new ImplicitValue("converted"); + } + } +} diff --git a/integration-tests/smallrye-config/src/main/java/io/quarkus/it/smallrye/config/ImplicitConvertersResource.java b/integration-tests/smallrye-config/src/main/java/io/quarkus/it/smallrye/config/ImplicitConvertersResource.java new file mode 100644 index 0000000000000..0891054c2da2e --- /dev/null +++ b/integration-tests/smallrye-config/src/main/java/io/quarkus/it/smallrye/config/ImplicitConvertersResource.java @@ -0,0 +1,30 @@ +package io.quarkus.it.smallrye.config; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +@Path("/implicit/converters") +public class ImplicitConvertersResource { + @Inject + ImplicitConverters implicitConverters; + + @GET + @Path("/optional") + public Response optional() { + return Response.ok(implicitConverters.optional().get().getValue()).build(); + } + + @GET + @Path("/list") + public Response list() { + return Response.ok(implicitConverters.list().get(0).getValue()).build(); + } + + @GET + @Path("/map") + public Response map() { + return Response.ok(implicitConverters.map().get("key").getValue()).build(); + } +} diff --git a/integration-tests/smallrye-config/src/main/resources/application.properties b/integration-tests/smallrye-config/src/main/resources/application.properties index 960766adbc554..031da5a495de2 100644 --- a/integration-tests/smallrye-config/src/main/resources/application.properties +++ b/integration-tests/smallrye-config/src/main/resources/application.properties @@ -28,3 +28,6 @@ smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key=somearbitrarycra smallrye.config.source.keystore.test.path=keystore smallrye.config.source.keystore.test.password=secret smallrye.config.source.keystore.test.handler=aes-gcm-nopadding + +#implicit converters +implicit.converters.map."key"=value diff --git a/integration-tests/smallrye-config/src/test/java/io/quarkus/it/smallrye/config/ImplicitConvertersIT.java b/integration-tests/smallrye-config/src/test/java/io/quarkus/it/smallrye/config/ImplicitConvertersIT.java new file mode 100644 index 0000000000000..2c9f9acb2d36f --- /dev/null +++ b/integration-tests/smallrye-config/src/test/java/io/quarkus/it/smallrye/config/ImplicitConvertersIT.java @@ -0,0 +1,8 @@ +package io.quarkus.it.smallrye.config; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class ImplicitConvertersIT extends ImplicitConvertersTest { + +} diff --git a/integration-tests/smallrye-config/src/test/java/io/quarkus/it/smallrye/config/ImplicitConvertersTest.java b/integration-tests/smallrye-config/src/test/java/io/quarkus/it/smallrye/config/ImplicitConvertersTest.java new file mode 100644 index 0000000000000..f69a1dda01ef4 --- /dev/null +++ b/integration-tests/smallrye-config/src/test/java/io/quarkus/it/smallrye/config/ImplicitConvertersTest.java @@ -0,0 +1,39 @@ +package io.quarkus.it.smallrye.config; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.Response.Status.OK; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class ImplicitConvertersTest { + @Test + void optional() { + given() + .get("/implicit/converters/optional") + .then() + .statusCode(OK.getStatusCode()) + .body(is("converted")); + } + + @Test + void list() { + given() + .get("/implicit/converters/list") + .then() + .statusCode(OK.getStatusCode()) + .body(is("converted")); + } + + @Test + void map() { + given() + .get("/implicit/converters/map") + .then() + .statusCode(OK.getStatusCode()) + .body(is("converted")); + } +}