diff --git a/all/pom.xml b/all/pom.xml index a4d6e458d54..565604cf210 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -1129,16 +1129,8 @@ helidon-service-codegen - io.helidon.service.inject - helidon-service-inject-codegen - - - io.helidon.service.inject - helidon-service-inject-api - - - io.helidon.service.inject - helidon-service-inject + io.helidon.service + helidon-service-maven-plugin io.helidon.metadata diff --git a/applications/parent/pom.xml b/applications/parent/pom.xml index a6d26ad323b..714d0b1acd3 100644 --- a/applications/parent/pom.xml +++ b/applications/parent/pom.xml @@ -212,6 +212,11 @@ + + io.helidon.service + helidon-service-project + ${helidon.version} + diff --git a/bom/pom.xml b/bom/pom.xml index 60d895a39ed..a5b56360fa0 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -1492,18 +1492,8 @@ ${helidon.version} - io.helidon.service.inject - helidon-service-inject-codegen - ${helidon.version} - - - io.helidon.service.inject - helidon-service-inject-api - ${helidon.version} - - - io.helidon.service.inject - helidon-service-inject + io.helidon.service + helidon-service-maven-plugin ${helidon.version} diff --git a/builder/api/pom.xml b/builder/api/pom.xml index 7e6a6158597..466188d9a03 100644 --- a/builder/api/pom.xml +++ b/builder/api/pom.xml @@ -36,11 +36,6 @@ io.helidon.common helidon-common - - io.helidon.common - helidon-common-config - true - diff --git a/builder/api/src/main/java/io/helidon/builder/api/Option.java b/builder/api/src/main/java/io/helidon/builder/api/Option.java index 96f6143be93..dc7a753ded2 100644 --- a/builder/api/src/main/java/io/helidon/builder/api/Option.java +++ b/builder/api/src/main/java/io/helidon/builder/api/Option.java @@ -31,7 +31,7 @@ private Option() { } /** - * Mark a prototype option as one that can be read from {@link io.helidon.common.config.Config}. + * Mark a prototype option as one that can be read from {@code io.helidon.common.config.Config}. */ @Target(ElementType.METHOD) @Inherited @@ -152,6 +152,30 @@ private Option() { boolean discoverServices() default true; } + /** + * Options annotated with this annotation will use service registry to discover instances to use. + * This annotation cannot be combined with {@link io.helidon.builder.api.Option.Configured} - if you want + * providers configured from configuration, kindly use {@link io.helidon.builder.api.Option.Provider}. + *

+ * Behavior depends on the return type of the annotated method: + *

+ * + * Options annotated with this annotation will load the instances as the default value (before method {@code builder()}) + * returns, thus you have full control over the field, be it an Optional, single value, or a List. + *

+ * To support usage of custom service registry, a {@code builder(ServiceRegistry)} method will be generated as well. + */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.CLASS) + public @interface RegistryService { + } + /** * Allowed values for this option. * The allowed value is always configured as a string, and is compared to {@link java.lang.String#valueOf(Object)} of the diff --git a/builder/api/src/main/java/io/helidon/builder/api/Prototype.java b/builder/api/src/main/java/io/helidon/builder/api/Prototype.java index 7c8cccd86c0..064459f6b22 100644 --- a/builder/api/src/main/java/io/helidon/builder/api/Prototype.java +++ b/builder/api/src/main/java/io/helidon/builder/api/Prototype.java @@ -21,13 +21,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.List; -import java.util.Optional; - -import io.helidon.common.HelidonServiceLoader; -import io.helidon.common.config.Config; -import io.helidon.common.config.ConfiguredProvider; -import io.helidon.common.config.NamedService; /** * Prototype is generated from a prototype blueprint, and it is expected to be part of the public API of the module. @@ -72,102 +65,6 @@ default BUILDER self() { } } - /** - * Extension of {@link io.helidon.builder.api.Prototype.Builder} that supports configuration. - * If a blueprint is marked as {@code @Configured}, build will accept configuration. - * - * @param type of the builder - * @param type of the prototype to be built - */ - public interface ConfiguredBuilder extends Builder { - /** - * Update builder from configuration. - * Any configured option that is defined on this prototype will be checked in configuration, and if it exists, - * it will override current value for that option on this builder. - * Options that do not exist in the provided config will not impact current values. - * The config instance is kept and may be used in builder decorator, it is not available in prototype implementation. - * - * @param config configuration to use - * @return updated builder instance - */ - BUILDER config(Config config); - - /** - * Discover services from configuration. - * If already configured instances already contain a service of the same type and name that would be added from - * configuration, the configuration would be ignored (e.g. the user must make a choice whether to configure, or - * set using an API). - * - * @param config configuration located at the parent node of the service providers - * @param configKey configuration key of the provider list - * (either a list node, or object, where each child is one service) - * @param serviceLoader helidon service loader for the expected type - * @param providerType type of the service provider interface - * @param configType type of the configured service - * @param allFromServiceLoader whether all services from service loader should be used, or only the ones with configured - * node - * @param existingInstances already configured instances - * @param type of the expected service - * @param type of the configured service provider that creates instances of S - * @return list of discovered services, ordered by {@link io.helidon.common.Weight} (highest weight is first in the list) - */ - default > List - discoverServices(Config config, - String configKey, - HelidonServiceLoader serviceLoader, - Class providerType, - Class configType, - boolean allFromServiceLoader, - List existingInstances) { - return ProvidedUtil.discoverServices(config, - configKey, - serviceLoader, - providerType, - configType, - allFromServiceLoader, - existingInstances); - } - - /** - * Discover service from configuration. If an instance is already configured using a builder, it will not be - * discovered from configuration (e.g. the user must make a choice whether to configure, or set using API). - * - * @param config configuration located at the parent node of the service providers - * @param configKey configuration key of the provider list - * (either a list node, or object, where each child is one service - this method requires - * * zero to one configured services) - * @param serviceLoader helidon service loader for the expected type - * @param providerType type of the service provider interface - * @param configType type of the configured service - * @param allFromServiceLoader whether all services from service loader should be used, or only the ones with configured - * node - * @param existingValue value already configured, if the name is same as discovered from configuration - * @param type of the expected service - * @param type of the configured service provider that creates instances of S - * @return the first service (ordered by {@link io.helidon.common.Weight} that is discovered, or empty optional if none - * is found - */ - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - default > Optional - discoverService(Config config, - String configKey, - HelidonServiceLoader serviceLoader, - Class providerType, - Class configType, - boolean allFromServiceLoader, - Optional existingValue) { - return ProvidedUtil.discoverService(config, - configKey, - serviceLoader, - providerType, - configType, - allFromServiceLoader, - existingValue); - } - - - } - /** * A prototype {@link Prototype.Blueprint} may extend this interface * to explicitly reference the associated runtime type. @@ -262,7 +159,7 @@ public interface Factory { /** * A blueprint annotated with this annotation will create a prototype that can be created from a - * {@link io.helidon.common.config.Config} instance. The builder will also have a method {@code config(Config)} that + * {@code io.helidon.common.config.Config} instance. The builder will also have a method {@code config(Config)} that * reads all options annotated with {@link io.helidon.builder.api.Option.Configured} from the config. */ @Target(ElementType.TYPE) diff --git a/builder/api/src/main/java/module-info.java b/builder/api/src/main/java/module-info.java index 2075aeb13e0..f65d56e0b9b 100644 --- a/builder/api/src/main/java/module-info.java +++ b/builder/api/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,5 @@ requires transitive io.helidon.common; - requires static io.helidon.common.config; - exports io.helidon.builder.api; - } diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataOption.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataOption.java index 8f0576be0ae..66a64cb6f43 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataOption.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/AnnotationDataOption.java @@ -45,6 +45,7 @@ import static io.helidon.builder.codegen.Types.OPTION_DEFAULT_METHOD; import static io.helidon.builder.codegen.Types.OPTION_PROVIDER; import static io.helidon.builder.codegen.Types.OPTION_REDUNDANT; +import static io.helidon.builder.codegen.Types.OPTION_REGISTRY_SERVICE; import static io.helidon.builder.codegen.Types.OPTION_REQUIRED; import static io.helidon.builder.codegen.Types.OPTION_SAME_GENERIC; import static io.helidon.builder.codegen.Types.OPTION_SINGULAR; @@ -60,6 +61,7 @@ record AnnotationDataOption(Javadoc javadoc, AccessModifier accessModifier, boolean required, boolean validateNotNull, + boolean registryService, boolean provider, TypeName providerType, boolean providerDiscoverServices, @@ -101,6 +103,7 @@ static AnnotationDataOption create(TypeHandler handler, TypedElementInfo element .orElse(false); } accessModifier = accessModifier(element); + boolean registryService = element.hasAnnotation(OPTION_REGISTRY_SERVICE); if (element.hasAnnotation(OPTION_PROVIDER)) { Annotation annotation = element.annotation(OPTION_PROVIDER); providerBased = true; @@ -171,6 +174,7 @@ static AnnotationDataOption create(TypeHandler handler, TypedElementInfo element accessModifier, required, validateNotNull, + registryService, providerBased, providerType, discoverServices, diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java index 22e13bb88b5..31249dfd782 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java @@ -200,6 +200,7 @@ private static void addBuilderMethod(ClassModel.Builder classModel, TypeName builderTypeName, List typeArguments, String ifaceName) { + classModel.addMethod(builder -> { builder.isStatic(true) .name("builder") @@ -438,7 +439,7 @@ private void process(RoundContext roundContext, TypeInfo blueprint) { blueprint.typeName(), blueprint.originatingElementValue()); - if (typeContext.typeInfo().supportsServiceRegistry() && typeContext.propertyData().hasProvider()) { + if (typeContext.typeInfo().supportsServiceRegistry()) { for (PrototypeProperty property : typeContext.propertyData().properties()) { if (property.configuredOption().provider()) { this.serviceLoaderContracts.add(property.configuredOption().providerType().genericTypeName().fqName()); diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/CustomMethods.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/CustomMethods.java index 03d919f9125..8f75293ca09 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/CustomMethods.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/CustomMethods.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -129,7 +130,8 @@ private static GeneratedMethod prototypeMethod(Errors.Collector errors, customMethod.returnType(), generatedArgs, // todo the javadoc may differ (such as when we have an additional parameter for instance methods) - customMethod.javadoc()), + customMethod.javadoc(), + customMethod.typeParameters()), annotations, codeGenerator); } @@ -178,7 +180,8 @@ private static GeneratedMethod builderMethod(Errors.Collector errors, customMethod.name(), typeInformation.prototypeBuilder(), generatedArgs, - customMethod.javadoc()), + customMethod.javadoc(), + customMethod.typeParameters), annotations, codeGenerator); } @@ -227,7 +230,8 @@ private static GeneratedMethod factoryMethod(Errors.Collector errors, customMethod.name(), customMethod.returnType(), customMethod.arguments(), - customMethod.javadoc()), + customMethod.javadoc(), + customMethod.typeParameters()), annotations, codeGenerator); } @@ -314,7 +318,26 @@ private static List findMethods(TypeContext.TypeInformation typeIn .filter(Predicate.not(String::isBlank)) // we do not care about blank values .toList(); - Method customMethod = new Method(customMethodsType.typeName(), methodName, returnType, arguments, javadoc); + List typeNames = it.typeParameters(); + List typeParametersToUse = new ArrayList<>(); + if (!typeNames.isEmpty()) { + // look for type parameters that differ in name from the ones defined by the blueprint + Set usedNames = typeInformation.blueprintType().declaredType() + .typeArguments() + .stream() + .map(TypeName::className) + .collect(Collectors.toSet()); + typeNames.stream() + .filter(methodTypeParam -> !usedNames.contains(methodTypeParam.className())) + .forEach(typeParametersToUse::add); + } + + Method customMethod = new Method(customMethodsType.typeName(), + methodName, + returnType, + arguments, + javadoc, + typeParametersToUse); return new CustomMethod(customMethod, methodProcessor.process(errors, @@ -343,7 +366,8 @@ record Method(TypeName declaringType, String name, TypeName returnType, List arguments, - List javadoc) { + List javadoc, + List typeParameters) { } diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java index 4da0d457c6f..41961f09792 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java @@ -44,6 +44,8 @@ import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNames; +import static io.helidon.builder.codegen.Types.CONFIG_BUILDER_SUPPORT; +import static io.helidon.builder.codegen.Types.REGISTRY_BUILDER_SUPPORT; import static io.helidon.codegen.CodegenUtil.capitalize; import static io.helidon.common.types.TypeNames.LIST; import static io.helidon.common.types.TypeNames.MAP; @@ -92,7 +94,7 @@ static void generate(ClassModel.Builder classModel, if (typeContext.configuredData().configured() || hasConfig(typeContext.propertyData().properties())) { builder.addInterface(TypeName.builder() - .from(Types.PROTOTYPE_CONFIGURED_BUILDER) + .from(Types.CONFIG_CONFIGURED_BUILDER) .addTypeArgument(TypeName.createFromGenericDeclaration("BUILDER")) .addTypeArgument(TypeName.createFromGenericDeclaration("PROTOTYPE")) .build()); @@ -183,10 +185,27 @@ private static void addCustomBuilderMethods(TypeContext typeContext, InnerClass. for (String annotation : customMethod.generatedMethod().annotations()) { method.addAnnotation(Annotation.parse(annotation)); } - for (CustomMethods.Argument argument : generated.arguments()) { - method.addParameter(param -> param.name(argument.name()) - .type(argument.typeName())); + int argSize = generated.arguments().size(); + for (int i = 0; i < argSize; i++) { + CustomMethods.Argument argument = generated.arguments().get(i); + + if (i == argSize - 1 && argument.typeName().array() && !argument.typeName().primitive()) { + // last argument + method.addParameter(param -> param.name(argument.name()) + .type(TypeName.builder(argument.typeName()) + .array(false) + .build()) + .vararg(true)); + } else { + method.addParameter(param -> param.name(argument.name()) + .type(argument.typeName())); + } } + + for (TypeName typeParameter : customMethod.generatedMethod().method().typeParameters()) { + method.addGenericArgument(TypeArgument.create(typeParameter)); + } + if (!generated.javadoc().isEmpty()) { Javadoc javadoc = Javadoc.builder() .from(Javadoc.parse(generated.javadoc())) @@ -194,6 +213,7 @@ private static void addCustomBuilderMethods(TypeContext typeContext, InnerClass. .build(); method.javadoc(javadoc); } + builder.addMethod(method); } } @@ -222,7 +242,7 @@ private static void builderMethods(InnerClass.Builder classBuilder, TypeContext TypeName returnType = TypeName.createFromGenericDeclaration("BUILDER"); - if (typeContext.propertyData().hasProvider() && typeContext.typeInfo().supportsServiceRegistry()) { + if (typeContext.typeInfo().supportsServiceRegistry()) { // generate setter for service registry serviceRegistrySetter(classBuilder); } @@ -304,8 +324,8 @@ public BUILDER config(Config config) { } */ Javadoc javadoc = Javadoc.builder() - .addLine("Provide an explicit registry instance to use. ") - .addLine("

") + .addLine("Provide an explicit registry instance to use.") + .addLine("

") .addLine("If not configured, the {@link " + Types.GLOBAL_SERVICE_REGISTRY.fqName() + "} would be used to discover services.") @@ -366,6 +386,7 @@ public BUILDER config(Config config) { if (configured.configured()) { for (PrototypeProperty child : properties) { if (child.configuredOption().configured() && !child.configuredOption().provider()) { + // registry service can never reach here, as they do not support Option.Configured child.typeHandler().generateFromConfig(builder, child.configuredOption(), child.factoryMethods()); @@ -422,7 +443,7 @@ private static void fromInstanceMethod(InnerClass.Builder builder, TypeContext t methodBuilder.addContentLine("());"); } } - if (property.configuredOption().provider()) { + if (property.configuredOption().provider() || property.registryService()) { methodBuilder.addContentLine(property.typeHandler().name() + "DiscoverServices = false;"); } } @@ -500,7 +521,7 @@ private static void fromBuilderMethod(InnerClass.Builder classBuilder, methodBuilder.addContentLine(setterName + "(builder." + getterName + "());"); } } - if (property.configuredOption().provider()) { + if (property.configuredOption().provider() || property.registryService()) { methodBuilder.addContent(property.name() + "DiscoverServices"); methodBuilder.addContentLine(" = builder." + property.name() + "DiscoverServices;"); } @@ -513,9 +534,7 @@ private static void fields(InnerClass.Builder classBuilder, TypeContext typeCont if (isBuilder && (typeContext.configuredData().configured() || hasConfig(typeContext.propertyData().properties()))) { classBuilder.addField(builder -> builder.type(Types.COMMON_CONFIG).name("config")); } - if (isBuilder - && typeContext.typeInfo().supportsServiceRegistry() - && typeContext.propertyData().hasProvider()) { + if (isBuilder && typeContext.typeInfo().supportsServiceRegistry()) { classBuilder.addField(builder -> builder.type(Types.SERVICE_REGISTRY).name("serviceRegistry")); } for (PrototypeProperty child : typeContext.propertyData().properties()) { @@ -538,11 +557,16 @@ private static void fields(InnerClass.Builder classBuilder, TypeContext typeCont if (!isBuilder || !isConfigProperty(child)) { classBuilder.addField(child.fieldDeclaration(isBuilder)); } - if (isBuilder && child.configuredOption().provider()) { + if (isBuilder && (child.configuredOption().provider())) { classBuilder.addField(builder -> builder.type(boolean.class) .name(child.name() + "DiscoverServices") .defaultValue(String.valueOf(child.configuredOption().providerDiscoverServices()))); } + if (isBuilder && (child.registryService())) { + classBuilder.addField(builder -> builder.type(boolean.class) + .name(child.name() + "DiscoverServices") + .defaultValue("true")); + } if (isBuilder && child.typeHandler().declaredType().isList()) { classBuilder.addField(builder -> builder.type(boolean.class) .name("is" + capitalize(child.name()) + "Mutated") @@ -569,20 +593,20 @@ private static void preBuildPrototypeMethod(InnerClass.Builder classBuilder, typeContext.typeInfo() .superPrototype() .ifPresent(it -> preBuildBuilder.addContentLine("super.preBuildPrototype();")); - if (typeContext.propertyData().hasProvider()) { + if (typeContext.typeInfo().supportsServiceRegistry() || typeContext.propertyData().hasProvider()) { boolean configured = typeContext.configuredData().configured(); - if (configured) { + if (configured && typeContext.propertyData().hasProvider()) { // need to have a non-null config instance preBuildBuilder.addContent("var config = this.config == null ? ") - .addContent(Types.COMMON_CONFIG) - .addContentLine(".empty() : this.config;"); + .addContent(Types.COMMON_CONFIG) + .addContentLine(".empty() : this.config;"); } if (typeContext.typeInfo().supportsServiceRegistry()) { - preBuildBuilder.addContent("var registry = this.serviceRegistry == null ? ") - .addContent(Types.GLOBAL_SERVICE_REGISTRY) - .addContentLine(".registry() : this.serviceRegistry;"); + preBuildBuilder.addContent("var registry = ") + .addContent(Optional.class) + .addContentLine(".ofNullable(this.serviceRegistry);"); } for (PrototypeProperty property : typeContext.propertyData().properties()) { @@ -611,6 +635,9 @@ private static void preBuildPrototypeMethod(InnerClass.Builder classBuilder, defaultDiscoverServices); } + } else if (property.registryService()) { + serviceRegistryProperty(preBuildBuilder, + property); } } } @@ -641,7 +668,9 @@ private static void serviceLoaderPropertyDiscovery(Method.Builder preBuildBuilde if (typeName.isList() || typeName.isSet()) { preBuildBuilder.addContent("this.add") .addContent(capitalize(property.name())) - .addContent("(discoverServices(config, \"") + .addContent("(") + .addContent(CONFIG_BUILDER_SUPPORT) + .addContent(".discoverServices(config, \"") .addContent(configuredOption.configKey()) .addContent("\", serviceLoader, ") .addContent(providerType.genericTypeName()) @@ -653,7 +682,9 @@ private static void serviceLoaderPropertyDiscovery(Method.Builder preBuildBuilde .addContent(property.name()) .addContentLine("));"); } else { - preBuildBuilder.addContent("discoverService(config, \"") + preBuildBuilder + .addContent(CONFIG_BUILDER_SUPPORT) + .addContent(".discoverService(config, \"") .addContent(configuredOption.configKey()) .addContent("\", serviceLoader, ") .addContent(providerType) @@ -677,6 +708,48 @@ private static void serviceLoaderPropertyDiscovery(Method.Builder preBuildBuilde preBuildBuilder.addContentLine("}"); } + private static void serviceRegistryProperty(Method.Builder preBuildBuilder, + PrototypeProperty property) { + TypeName typeName = property.typeHandler().declaredType(); + if (typeName.isList()) { + preBuildBuilder + .addContent("this.add") + .addContent(capitalize(property.name())) + .addContent("(") + .addContent(REGISTRY_BUILDER_SUPPORT) + .addContent(".serviceList(registry, ") + .addContentCreate(property.typeHandler().actualType()) + .addContent(", ") + .addContent(property.name()) + .addContentLine("DiscoverServices));"); + } else if (typeName.isSet()) { + preBuildBuilder + .addContent("this.add") + .addContent(capitalize(property.name())) + .addContent("(") + .addContent(REGISTRY_BUILDER_SUPPORT) + .addContent(".serviceSet(registry, ") + .addContentCreate(property.typeHandler().actualType()) + .addContent(", ") + .addContent(property.name()) + .addContentLine("DiscoverServices));"); + } else { + preBuildBuilder + .addContent(REGISTRY_BUILDER_SUPPORT) + .addContent(".service(registry, ") + .addContentCreate(property.typeHandler().actualType()) + .addContent(", ") + .addContent(Optional.class) + .addContent(".ofNullable(") + .addContent(property.name()) + .addContent("), ") + .addContent(property.name()) + .addContent("DiscoverServices).ifPresent(this::") + .addContent(property.setterName()) + .addContentLine(");"); + } + } + private static void serviceRegistryPropertyDiscovery(Method.Builder preBuildBuilder, PrototypeProperty property, boolean propertyConfigured, @@ -689,7 +762,7 @@ private static void serviceRegistryPropertyDiscovery(Method.Builder preBuildBuil preBuildBuilder.addContent("this.add") .addContent(capitalize(property.name())) .addContent("(") - .addContent(Types.GENERATED_SERVICE) + .addContent(CONFIG_BUILDER_SUPPORT) .addContentLine(".discoverServices(config,") .increaseContentPadding() .increaseContentPadding() @@ -711,7 +784,7 @@ private static void serviceRegistryPropertyDiscovery(Method.Builder preBuildBuil .decreaseContentPadding(); } else { preBuildBuilder - .addContent(Types.GENERATED_SERVICE) + .addContent(CONFIG_BUILDER_SUPPORT) .addContentLine(".discoverService(config,") .increaseContentPadding() .increaseContentPadding() @@ -1039,8 +1112,9 @@ private static void hashCodeMethod(InnerClass.Builder classBuilder, } method.addContent(equalityFields.stream() - .filter(it -> !(it.typeName().isOptional() - && it.typeHandler().actualType().equals(Types.CHAR_ARRAY))) + .filter(it -> !( + it.typeName().isOptional() + && it.typeHandler().actualType().equals(Types.CHAR_ARRAY))) .map(PrototypeProperty::name) .collect(Collectors.joining(", "))) .addContent(")"); diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/PrototypeProperty.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/PrototypeProperty.java index 2488dea8a4c..9e4631a24b6 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/PrototypeProperty.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/PrototypeProperty.java @@ -39,7 +39,8 @@ record PrototypeProperty(MethodSignature signature, FactoryMethods factoryMethods, boolean equality, // part of equals and hash code boolean toStringValue, // part of toString - boolean confidential // if part of toString, do not print the actual value + boolean confidential, // if part of toString, do not print the actual value, + boolean registryService ) { // cannot be identifiers - such as field name or method name private static final Set RESERVED_WORDS = Set.of( @@ -101,6 +102,7 @@ static PrototypeProperty create(CodegenContext ctx, boolean equality = !redundantAnnotation.flatMap(it -> it.getValue("equality")) .map(Boolean::parseBoolean) .orElse(false); + boolean registryService = element.hasAnnotation(Types.OPTION_REGISTRY_SERVICE); return new PrototypeProperty( MethodSignature.create(element), @@ -109,7 +111,8 @@ static PrototypeProperty create(CodegenContext ctx, factoryMethods, equality, toStringValue, - confidential + confidential, + registryService ); } diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeContext.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeContext.java index 12b74b32555..13a8614340f 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeContext.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeContext.java @@ -203,9 +203,21 @@ static TypeContext create(CodegenContext ctx, TypeInfo blueprint) { .className("Builder") .build(); + /* + Service registry is supported if it is explicitly configured && a provider exists, + or there is an option annotated with @Option.RegistryService + */ boolean supportsServiceRegistry = blueprint.findAnnotation(Types.PROTOTYPE_SERVICE_REGISTRY) .flatMap(Annotation::booleanValue) - .orElse(false); + .orElse(false) + && blueprint.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .anyMatch(ElementInfoPredicates.hasAnnotation(Types.OPTION_PROVIDER)); + supportsServiceRegistry |= blueprint.elementInfo() + .stream() + .filter(ElementInfoPredicates::isMethod) + .anyMatch(ElementInfoPredicates.hasAnnotation(Types.OPTION_REGISTRY_SERVICE)); TypeInformation typeInformation = new TypeInformation(supportsServiceRegistry, blueprint, diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java index c67e0d0ef6f..e8f7643acd0 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandler.java @@ -41,6 +41,7 @@ import io.helidon.common.types.TypedElementInfo; import static io.helidon.builder.codegen.Types.OPTION_DEFAULT; +import static io.helidon.builder.codegen.Types.SERVICES; import static io.helidon.common.types.TypeNames.BOXED_BOOLEAN; import static io.helidon.common.types.TypeNames.BOXED_BYTE; import static io.helidon.common.types.TypeNames.BOXED_CHAR; @@ -164,6 +165,34 @@ public String toString() { return declaredType.fqName() + " " + name; } + void updateBuilderFromServices(ContentBuilder content, String builder) { + /* + Services.first(Type.class).ifPresent(builder::option); + */ + content.addContent(SERVICES) + .addContent(".first(") + .addContent(actualType()) + .addContent(".class).ifPresent(") + .addContent(builder) + .addContent("::") + .addContent(setterName()) + .addContentLine(");"); + } + + void updateBuilderFromRegistry(ContentBuilder content, String builder, String registry) { + /* + registry.first(Type.class).ifPresent(builder::option); + */ + content.addContent(registry) + .addContent(".first(") + .addContent(actualType()) + .addContent(".class).ifPresent(") + .addContent(builder) + .addContent("::") + .addContent(setterName()) + .addContentLine(");"); + } + TypeName builderGetterType(boolean required, boolean hasDefault) { if (builderGetterOptional(required, hasDefault)) { if (declaredType().isOptional()) { diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java index 31ebdb6acca..1aacb6514c5 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerCollection.java @@ -256,7 +256,7 @@ void setters(InnerClass.Builder classBuilder, TypeName returnType, Javadoc blueprintJavadoc) { - if (configured.provider()) { + if (configured.provider() || configured.registryService()) { discoverServicesSetter(classBuilder, configured, returnType, blueprintJavadoc); } @@ -265,8 +265,11 @@ void setters(InnerClass.Builder classBuilder, declaredSetters(classBuilder, configured, returnType, blueprintJavadoc); if (factoryMethods.createTargetType().isPresent()) { - // if there is a factory method for the return type, we also have setters for the type (probably config object) - factorySetter(classBuilder, configured, returnType, blueprintJavadoc, factoryMethods.createTargetType().get()); + FactoryMethods.FactoryMethod factoryMethod = factoryMethods.createTargetType().get(); + if (factoryMethod.factoryMethodReturnType().isList() || factoryMethod.factoryMethodReturnType().isSet()) { + // if there is a factory method for the return type, we also have setters for the type (probably config object) + factorySetter(classBuilder, configured, returnType, blueprintJavadoc, factoryMethod); + } } if (configured.singular()) { diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerList.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerList.java index 3f6273d9d9b..e1c3190021f 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerList.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerList.java @@ -19,10 +19,12 @@ import java.util.Optional; import io.helidon.codegen.CodegenUtil; +import io.helidon.codegen.classmodel.ContentBuilder; import io.helidon.codegen.classmodel.Method; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypedElementInfo; +import static io.helidon.builder.codegen.Types.SERVICES; import static io.helidon.common.types.TypeNames.LIST; class TypeHandlerList extends TypeHandlerCollection { @@ -47,6 +49,36 @@ Method.Builder extraSetterContent(Method.Builder builder) { return builder.addContentLine(isMutatedField() + " = true;"); } + @Override + void updateBuilderFromServices(ContentBuilder content, String builder) { + /* + builder.option(Services.all(Type.class)); + */ + content.addContent(builder) + .addContent(".") + .addContent(setterName()) + .addContent("(") + .addContent(SERVICES) + .addContent(".all(") + .addContent(actualType()) + .addContentLine(".class));"); + } + + @Override + void updateBuilderFromRegistry(ContentBuilder content, String builder, String registry) { + /* + builder.option(registry.all(Type.class)); + */ + content.addContent(builder) + .addContent(".") + .addContent(setterName()) + .addContent("(") + .addContent(registry) + .addContent(".all(") + .addContent(actualType()) + .addContentLine(".class));"); + } + private String isMutatedField() { return isMutatedField(name()); } diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSet.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSet.java index 20a68b5a7bf..32187796ff8 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSet.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/TypeHandlerSet.java @@ -18,9 +18,12 @@ import java.util.Optional; +import io.helidon.codegen.classmodel.ContentBuilder; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypedElementInfo; +import static io.helidon.builder.codegen.Types.LINKED_HASH_SET; +import static io.helidon.builder.codegen.Types.SERVICES; import static io.helidon.common.types.TypeNames.SET; class TypeHandlerSet extends TypeHandlerCollection { @@ -38,4 +41,38 @@ class TypeHandlerSet extends TypeHandlerCollection { "collect(java.util.stream.Collectors.toSet())", Optional.of(".map(java.util.Set::copyOf)")); } + + @Override + void updateBuilderFromServices(ContentBuilder content, String builder) { + /* + builder.option(new LinkedHashSet(Services.all(Type.class))); + */ + content.addContent(builder) + .addContent(".") + .addContent(setterName()) + .addContent("(new ") + .addContent(LINKED_HASH_SET) + .addContent("(") + .addContent(SERVICES) + .addContent(".all(") + .addContent(actualType()) + .addContentLine(".class)));"); + } + + @Override + void updateBuilderFromRegistry(ContentBuilder content, String builder, String registry) { + /* + builder.option(new LinkedHashSet(registry.all(Type.class))); + */ + content.addContent(builder) + .addContent(".") + .addContent(setterName()) + .addContent("(new ") + .addContent(LINKED_HASH_SET) + .addContent("(") + .addContent(registry) + .addContent(".all(") + .addContent(actualType()) + .addContentLine(".class)));"); + } } diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java index 2b7b56131ca..f6c840325af 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java @@ -36,7 +36,7 @@ final class Types { static final TypeName URI = TypeName.create(java.net.URI.class); static final TypeName SERVICE_REGISTRY = TypeName.create("io.helidon.service.registry.ServiceRegistry"); static final TypeName GLOBAL_SERVICE_REGISTRY = TypeName.create("io.helidon.service.registry.GlobalServiceRegistry"); - static final TypeName GENERATED_SERVICE = TypeName.create("io.helidon.service.registry.GeneratedService"); + static final TypeName SERVICES = TypeName.create("io.helidon.service.registry.Services"); static final TypeName BUILDER_DESCRIPTION = TypeName.create("io.helidon.builder.api.Description"); @@ -48,7 +48,6 @@ final class Types { static final TypeName PROTOTYPE_CONFIGURED = TypeName.create("io.helidon.builder.api.Prototype.Configured"); static final TypeName PROTOTYPE_PROVIDES = TypeName.create("io.helidon.builder.api.Prototype.Provides"); static final TypeName PROTOTYPE_BUILDER = TypeName.create("io.helidon.builder.api.Prototype.Builder"); - static final TypeName PROTOTYPE_CONFIGURED_BUILDER = TypeName.create("io.helidon.builder.api.Prototype.ConfiguredBuilder"); static final TypeName PROTOTYPE_CUSTOM_METHODS = TypeName.create("io.helidon.builder.api.Prototype.CustomMethods"); static final TypeName PROTOTYPE_FACTORY_METHOD = TypeName.create("io.helidon.builder.api.Prototype.FactoryMethod"); static final TypeName PROTOTYPE_BUILDER_METHOD = TypeName.create("io.helidon.builder.api.Prototype.BuilderMethod"); @@ -72,6 +71,7 @@ final class Types { static final TypeName OPTION_PROVIDER = TypeName.create("io.helidon.builder.api.Option.Provider"); static final TypeName OPTION_ALLOWED_VALUES = TypeName.create("io.helidon.builder.api.Option.AllowedValues"); static final TypeName OPTION_ALLOWED_VALUE = TypeName.create("io.helidon.builder.api.Option.AllowedValue"); + static final TypeName OPTION_REGISTRY_SERVICE = TypeName.create("io.helidon.builder.api.Option.RegistryService"); static final TypeName OPTION_DEFAULT = TypeName.create("io.helidon.builder.api.Option.Default"); static final TypeName OPTION_DEFAULT_INT = TypeName.create("io.helidon.builder.api.Option.DefaultInt"); static final TypeName OPTION_DEFAULT_DOUBLE = TypeName.create("io.helidon.builder.api.Option.DefaultDouble"); @@ -83,6 +83,12 @@ final class Types { static final TypeName OPTION_TYPE = TypeName.create("io.helidon.builder.api.Option.Type"); static final TypeName OPTION_DECORATOR = TypeName.create("io.helidon.builder.api.Option.Decorator"); + static final TypeName CONFIG_CONFIGURED_BUILDER = TypeName.create( + "io.helidon.common.config.ConfigBuilderSupport.ConfiguredBuilder"); + static final TypeName CONFIG_BUILDER_SUPPORT = TypeName.create("io.helidon.common.config.ConfigBuilderSupport"); + + static final TypeName REGISTRY_BUILDER_SUPPORT = TypeName.create("io.helidon.service.registry.RegistryBuilderSupport"); + private Types() { } } diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/GenerateAbstractBuilder.java b/builder/processor/src/main/java/io/helidon/builder/processor/GenerateAbstractBuilder.java index 32d0b890c0f..00d199c98d1 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/GenerateAbstractBuilder.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/GenerateAbstractBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,10 +40,10 @@ import io.helidon.common.types.TypeName; import static io.helidon.builder.processor.Types.CHAR_ARRAY_TYPE; +import static io.helidon.builder.processor.Types.CONFIG_CONFIGURED_BUILDER; import static io.helidon.builder.processor.Types.CONFIG_TYPE; import static io.helidon.builder.processor.Types.OVERRIDE; import static io.helidon.builder.processor.Types.PROTOTYPE_BUILDER; -import static io.helidon.builder.processor.Types.PROTOTYPE_CONFIGURED_BUILDER; import static io.helidon.builder.processor.Types.STRING_TYPE; import static io.helidon.common.processor.GeneratorTools.capitalize; import static io.helidon.common.processor.classmodel.ClassModel.TYPE_TOKEN; @@ -93,7 +93,7 @@ static void generate(ClassModel.Builder classModel, if (typeContext.configuredData().configured() || hasConfig(typeContext.propertyData().properties())) { builder.addInterface(TypeName.builder() - .from(TypeName.create(PROTOTYPE_CONFIGURED_BUILDER)) + .from(CONFIG_CONFIGURED_BUILDER) .addTypeArgument(TypeName.createFromGenericDeclaration("BUILDER")) .addTypeArgument(TypeName.createFromGenericDeclaration("PROTOTYPE")) .build()); diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/Types.java b/builder/processor/src/main/java/io/helidon/builder/processor/Types.java index fda9f783f2c..9b66485ae1e 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/Types.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/Types.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,6 @@ final class Types { static final String FACTORY_METHOD = "io.helidon.builder.api.Prototype.FactoryMethod"; static final String PROTOTYPE_BUILDER = "io.helidon.builder.api.Prototype.Builder"; static final String PROTOTYPE_CONFIGURED = "io.helidon.builder.api.Prototype.Configured"; - static final String PROTOTYPE_CONFIGURED_BUILDER = "io.helidon.builder.api.Prototype.ConfiguredBuilder"; static final String PROTOTYPE_BLUEPRINT = "io.helidon.builder.api.Prototype.Blueprint"; static final String PROTOTYPE_FACTORY = "io.helidon.builder.api.Prototype.Factory"; static final String PROTOTYPE_ANNOTATED = "io.helidon.builder.api.Prototype.Annotated"; @@ -111,6 +110,10 @@ final class Types { static final TypeName LINKED_HASH_SET_TYPE = TypeName.create(LinkedHashSet.class); static final TypeName ARRAY_LIST_TYPE = TypeName.create(ArrayList.class); + static final TypeName CONFIG_CONFIGURED_BUILDER = TypeName.create( + "io.helidon.common.config.ConfigBuilderSupport.ConfiguredBuilder"); + static final TypeName CONFIG_BUILDER_SUPPORT = TypeName.create("io.helidon.common.config.ConfigBuilderSupport"); + private Types() { } } diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/WithProviderRegistryBlueprint.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/WithProviderRegistryBlueprint.java index 37d6a35c534..11b105483e3 100644 --- a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/WithProviderRegistryBlueprint.java +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/WithProviderRegistryBlueprint.java @@ -18,9 +18,11 @@ import java.util.List; import java.util.Optional; +import java.util.Set; import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; +import io.helidon.common.mapper.Mappers; @Prototype.Blueprint @Prototype.Configured @@ -68,4 +70,16 @@ interface WithProviderRegistryBlueprint { @Option.Configured @Option.Provider(value = ProviderNoImpls.class, discoverServices = false) List listNoImplNotDiscover(); + + @Option.RegistryService + Optional mappers(); + + @Option.RegistryService + Mappers mappersExplicit(); + + @Option.RegistryService + List mappersList(); + + @Option.RegistryService + Set mappersSet(); } diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/ProviderRegistryTest.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/ProviderRegistryTest.java index 186bb4da369..aabad49addc 100644 --- a/builder/tests/builder/src/test/java/io/helidon/builder/test/ProviderRegistryTest.java +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/ProviderRegistryTest.java @@ -21,6 +21,7 @@ import io.helidon.builder.test.testsubjects.SomeProvider; import io.helidon.builder.test.testsubjects.WithProviderRegistry; import io.helidon.common.Errors; +import io.helidon.common.mapper.Mappers; import io.helidon.config.Config; import io.helidon.config.ConfigSources; @@ -48,7 +49,10 @@ void testMinimalDefined() { /* Only the property that does not discover services and does not return optional is defined in config */ - WithProviderRegistry value = WithProviderRegistry.create(config.get("min-defined")); + WithProviderRegistry value = WithProviderRegistry.builder() + .config(config.get("min-defined")) + .mappersExplicit(Mappers.create()) + .build(); assertThat(value.oneDiscover().prop(), is("some-1")); assertThat(value.oneNotDiscover().prop(), is("config")); @@ -73,7 +77,10 @@ void testAllDefined() { /* Everything is customized */ - WithProviderRegistry value = WithProviderRegistry.create(config.get("all-defined")); + WithProviderRegistry value = WithProviderRegistry.builder() + .config(config.get("all-defined")) + .mappersExplicit(Mappers.create()) + .build(); assertThat(value.oneDiscover().prop(), is("config")); assertThat(value.oneNotDiscover().prop(), is("config")); @@ -98,7 +105,10 @@ void testFail() { Missing the one mandatory option in config */ Errors.ErrorMessagesException fail = assertThrows(Errors.ErrorMessagesException.class, - () -> WithProviderRegistry.create(config.get("fail"))); + () -> WithProviderRegistry.builder() + .config(config.get("fail")) + .mappersExplicit(Mappers.create()) + .build()); assertThat(fail.getMessages(), hasSize(1)); assertThat(fail.getMessage(), containsString("\"one-not-discover\" must not be null")); @@ -110,7 +120,10 @@ void testSingleList() { Single value list (when not using discovery). When discovery is used, all in service loader are discovered, even if not configured */ - WithProviderRegistry value = WithProviderRegistry.create(config.get("single-list")); + WithProviderRegistry value = WithProviderRegistry.builder() + .config(config.get("single-list")) + .mappersExplicit(Mappers.create()) + .build(); assertThat(value.oneNotDiscover().prop(), is("config2")); @@ -130,6 +143,7 @@ void testDisabledDiscoveryOnTheCopy() { WithProviderRegistry value = WithProviderRegistry.builder() .listDiscoverDiscoverServices(false) .oneNotDiscover(someService) //This needs to be set, otherwise validation fails + .mappersExplicit(Mappers.create()) .build(); assertThat(value.listDiscover(), is(List.of())); @@ -144,11 +158,13 @@ void testDisabledDiscoveryOnTheCopiedBuilder() { SomeProvider.SomeService someService = new DummyService(); WithProviderRegistry.Builder value = WithProviderRegistry.builder() .listDiscoverDiscoverServices(false) + .mappersExplicit(Mappers.create()) .oneNotDiscover(someService); WithProviderRegistry copy = WithProviderRegistry.builder() .from(value) .build(); + assertThat(copy.listDiscover(), is(List.of())); } diff --git a/builder/tests/codegen/src/test/java/io/helidon/builder/codegen/TypesTest.java b/builder/tests/codegen/src/test/java/io/helidon/builder/codegen/TypesTest.java index a2cc8559586..02788c1578f 100644 --- a/builder/tests/codegen/src/test/java/io/helidon/builder/codegen/TypesTest.java +++ b/builder/tests/codegen/src/test/java/io/helidon/builder/codegen/TypesTest.java @@ -35,10 +35,12 @@ import io.helidon.builder.api.RuntimeType; import io.helidon.common.Generated; import io.helidon.common.config.Config; +import io.helidon.common.config.ConfigBuilderSupport; import io.helidon.common.types.TypeName; -import io.helidon.service.registry.GeneratedService; import io.helidon.service.registry.GlobalServiceRegistry; +import io.helidon.service.registry.RegistryBuilderSupport; import io.helidon.service.registry.ServiceRegistry; +import io.helidon.service.registry.Services; import org.hamcrest.CoreMatchers; import org.hamcrest.collection.IsEmptyCollection; @@ -90,7 +92,6 @@ void testTypes() { checkField(toCheck, checked, fields, "URI", URI.class); checkField(toCheck, checked, fields, "SERVICE_REGISTRY", ServiceRegistry.class); checkField(toCheck, checked, fields, "GLOBAL_SERVICE_REGISTRY", GlobalServiceRegistry.class); - checkField(toCheck, checked, fields, "GENERATED_SERVICE", GeneratedService.class); checkField(toCheck, checked, fields, "BUILDER_DESCRIPTION", Description.class); checkField(toCheck, checked, fields, "PROTOTYPE_BLUEPRINT", Prototype.Blueprint.class); checkField(toCheck, checked, fields, "PROTOTYPE_IMPLEMENT", Prototype.Implement.class); @@ -100,7 +101,6 @@ void testTypes() { checkField(toCheck, checked, fields, "PROTOTYPE_CONFIGURED", Prototype.Configured.class); checkField(toCheck, checked, fields, "PROTOTYPE_PROVIDES", Prototype.Provides.class); checkField(toCheck, checked, fields, "PROTOTYPE_BUILDER", Prototype.Builder.class); - checkField(toCheck, checked, fields, "PROTOTYPE_CONFIGURED_BUILDER", Prototype.ConfiguredBuilder.class); checkField(toCheck, checked, fields, "PROTOTYPE_CUSTOM_METHODS", Prototype.CustomMethods.class); checkField(toCheck, checked, fields, "PROTOTYPE_FACTORY_METHOD", Prototype.FactoryMethod.class); checkField(toCheck, checked, fields, "PROTOTYPE_BUILDER_METHOD", Prototype.BuilderMethod.class); @@ -132,6 +132,14 @@ void testTypes() { checkField(toCheck, checked, fields, "OPTION_DEPRECATED", Option.Deprecated.class); checkField(toCheck, checked, fields, "OPTION_TYPE", Option.Type.class); checkField(toCheck, checked, fields, "OPTION_DECORATOR", Option.Decorator.class); + checkField(toCheck, checked, fields, "OPTION_REGISTRY_SERVICE", Option.RegistryService.class); + + checkField(toCheck, checked, fields, "SERVICES", Services.class); + + checkField(toCheck, checked, fields, "CONFIG_BUILDER_SUPPORT", ConfigBuilderSupport.class); + checkField(toCheck, checked, fields, "CONFIG_CONFIGURED_BUILDER", ConfigBuilderSupport.ConfiguredBuilder.class); + + checkField(toCheck, checked, fields, "REGISTRY_BUILDER_SUPPORT", RegistryBuilderSupport.class); assertThat(toCheck, IsEmptyCollection.empty()); } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignature.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignature.java index a167313516d..6e045d1c970 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignature.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignature.java @@ -45,6 +45,39 @@ public sealed interface ElementSignature permits ElementSignatures.FieldSignatur ElementSignatures.MethodSignature, ElementSignatures.ParameterSignature, ElementSignatures.NoSignature { + /** + * A field signature. + * + * @param type type of the field + * @param name name of the field + * @return a new field signature + */ + static ElementSignature createField(TypeName type, String name) { + return ElementSignatures.createField(type, name); + } + + /** + * A constructor signature. + * + * @param parameters list of types of parameters + * @return a new constructor signature + */ + static ElementSignature createConstructor(List parameters) { + return ElementSignatures.createConstructor(parameters); + } + + /** + * A method signature. + * + * @param returnType return type of the method + * @param name name of the method + * @param parameters parameter types of the method + * @return a new method signature + */ + static ElementSignature createMethod(TypeName returnType, String name, List parameters) { + return ElementSignatures.createMethod(returnType, name, parameters); + } + /** * Type of the element. Resolves as follows: *

+ * + * @deprecated use {@link io.helidon.common.mapper.Mappers} instead */ -public interface MapperManager { +@Deprecated(forRemoval = true, since = "4.2.0") +public interface MapperManager extends Mappers { /** * Get an instance of the configured global manager. If none is explicitly set, an instance is created using * discovered and built-in mappers. * * @return global mapper manager + * @deprecated use {@link io.helidon.service.registry.Services#get(Class)} - i.e. {@code Services.get(Mappers.class)}, or + * use your registry instance in a similar way */ + @Deprecated(forRemoval = true, since = "4.2.0") static MapperManager global() { return GlobalManager.mapperManager(); } @@ -54,7 +60,11 @@ static MapperManager global() { * Configure a new global mapper manager. * * @param manager mapper manager to use + * @deprecated use {@link io.helidon.service.registry.Services#set(Class, Object[])} - + * i.e. {@code Services.get(Mappers.class, myMappers)} before the program starts + * (must be configured before it is first used) */ + @Deprecated(forRemoval = true, since = "4.2.0") static void global(MapperManager manager) { GlobalManager.mapperManager(manager); } @@ -63,7 +73,9 @@ static void global(MapperManager manager) { * Create a fluent API builder to create a customized mapper manager. * * @return a new builder + * @deprecated use {@link io.helidon.common.mapper.Mappers#builder()} instead */ + @Deprecated(forRemoval = true, since = "4.2.0") static Builder builder() { return new Builder(); } @@ -73,7 +85,9 @@ static Builder builder() { * loaded {@link io.helidon.common.mapper.spi.MapperProvider MapperProviders}. * * @return create a new mapper manager from service loader + * @deprecated use {@link io.helidon.common.mapper.Mappers#create()} instead */ + @Deprecated(forRemoval = true, since = "4.2.0") static MapperManager create() { return MapperManager.builder().build(); } @@ -127,7 +141,10 @@ Optional> mapper(GenericType sou /** * Fluent API builder for {@link io.helidon.common.mapper.MapperManager}. + * + * @deprecated use {@link io.helidon.common.mapper.MappersConfig.Builder}, obtained from {@link Mappers#builder()} */ + @Deprecated(since = "4.2.0", forRemoval = true) final class Builder implements io.helidon.common.Builder { private HelidonServiceLoader.Builder providers = HelidonServiceLoader .builder(ServiceLoader.load(MapperProvider.class)); @@ -146,7 +163,7 @@ public MapperManager build() { } providers.addService(new DefaultMapperProvider(), 0); providers.useSystemServiceLoader(discoverServices); - return new MapperManagerImpl(this); + return new MappersImpl(this); } /** @@ -172,7 +189,9 @@ public Builder addMapperProvider(MapperProvider provider) { * documentation for details about priority handling) * @return updated builder instance * @see #addMapperProvider(io.helidon.common.mapper.spi.MapperProvider) + * @deprecated we have switched to using weights instead of priority, this method will be removed without replacement */ + @Deprecated(forRemoval = true, since = "4.2.0") public Builder addMapperProvider(MapperProvider provider, int priority) { this.providers.addService(provider, priority); return this; diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/Mappers.java b/common/mapper/src/main/java/io/helidon/common/mapper/Mappers.java new file mode 100644 index 00000000000..55bfb9f8341 --- /dev/null +++ b/common/mapper/src/main/java/io/helidon/common/mapper/Mappers.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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.helidon.common.mapper; + +import java.util.Optional; +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; +import io.helidon.common.GenericType; + +/** + * Mappers manager of all configured mappers. + *

+ * To map a source to target, you can use either of the {@code map} methods defined in this interface, + * as they make sure that the mapping exists in either space. + *

    + *
  • If you call {@link #map(Object, Class, Class, String...)} and no mapper is found for the class pair, + * the implementation calls the {@link #map(Object, io.helidon.common.GenericType, io.helidon.common.GenericType, String...)} + * with {@link io.helidon.common.GenericType}s created for each parameters
  • + *
  • If you call {@link #map(Object, io.helidon.common.GenericType, io.helidon.common.GenericType, String...)} and no mapper is + * found for the {@link io.helidon.common.GenericType} pair, an attempt is to locate a mapper for + * the underlying class *IF* the generic type represents a simple class (e.g. not a generic type declaration)
  • + *
+ */ +@RuntimeType.PrototypedBy(MappersConfig.class) +public interface Mappers extends RuntimeType.Api { + /** + * Create a new builder to customize configuration of {@link io.helidon.common.mapper.Mappers}. + * + * @return a new fluent API builder + */ + static MappersConfig.Builder builder() { + return MappersConfig.builder(); + } + + /** + * Create mappers using defaults. + * + * @return create new mappers + */ + static Mappers create() { + return builder().build(); + } + + /** + * Create new {@link io.helidon.common.mapper.Mappers} using the provided configuration. + * + * @param config mappers configuration + * @return a new mappers configured from the provided config + */ + static Mappers create(MappersConfig config) { + return new MappersImpl(config); + } + + /** + * Create new {@link io.helidon.common.mapper.Mappers} customizing its configuration. + * + * @param consumer consumer of configuration builder + * @return a new configured mappers instance + */ + static Mappers create(Consumer consumer) { + return builder() + .update(consumer) + .build(); + } + + /** + * Map from source to target. + * + * @param source object to map + * @param sourceType type of the source object (to locate the mapper) + * @param targetType type of the target object (to locate the mapper) + * @param qualifiers qualifiers of the usage (such as {@code http-headers, http}, most specific one first) + * @param type of the source + * @param type of the target + * @return result of the mapping + * @throws MapperException in case the mapper was not found or failed + */ + TARGET map(SOURCE source, + GenericType sourceType, + GenericType targetType, + String... qualifiers) + throws MapperException; + + /** + * Map from source to target. + * + * @param source object to map + * @param sourceType class of the source object (to locate the mapper) + * @param targetType class of the target object (to locate the mapper) + * @param qualifiers qualifiers of the usage (such as {@code http-headers, http}, most specific one first) + * @param type of the source + * @param type of the target + * @return result of the mapping + * @throws MapperException in case the mapper was not found or failed + */ + TARGET map(SOURCE source, Class sourceType, Class targetType, String... qualifiers) + throws MapperException; + + /** + * Obtain a mapper for the provided types and qualifiers. + * + * @param sourceType type to map from + * @param targetType type to map to + * @param qualifiers qualifiers of the mapper + * @param source type + * @param target type + * @return mapper if found + */ + Optional> mapper(GenericType sourceType, + GenericType targetType, + String... qualifiers); +} diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/MappersConfigBlueprint.java b/common/mapper/src/main/java/io/helidon/common/mapper/MappersConfigBlueprint.java new file mode 100644 index 00000000000..0fc452885f0 --- /dev/null +++ b/common/mapper/src/main/java/io/helidon/common/mapper/MappersConfigBlueprint.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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.helidon.common.mapper; + +import java.util.List; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.mapper.spi.MapperProvider; + +/** + * Mappers configuration. + *

+ * All mappers registered within this type (through its builder) are ordered by their weight, regardless of + * order of configuration on builder. + */ +@Prototype.Blueprint(decorator = MappersConfigSupport.BuilderDecorator.class) +@Prototype.RegistrySupport +@Prototype.CustomMethods(MappersConfigSupport.CustomMethods.class) +interface MappersConfigBlueprint extends Prototype.Factory { + /** + * Mapper providers allow for introduction of new mappers into the system. + * A mapper provider can be exposed either as a {@link io.helidon.service.registry.ServiceRegistry} + * service, or a {@link java.util.ServiceLoader}, but never both. + *

+ * Mapper providers are ordered by their {@link io.helidon.common.Weight}. + * + * @return a list of configured mapper providers + */ + @Option.Singular + @Option.RegistryService + List mapperProviders(); + + /** + * Mappers discovered through {@link io.helidon.service.registry.ServiceRegistry}, or explicitly created + * that implement both {@link Mapper#sourceType()} and {@link Mapper#targetType()} methods. + *

+ * Mappers are ordered by {@link io.helidon.common.Weight} together with {@link MappersConfig#mapperProviders()} to + * create a single list. + * + * @return list of mappers + */ + @Option.Singular + @Option.RegistryService + List> mappers(); + + /** + * Whether to use built-in mappers. + * Defaults to {@code true}. + * + * @return whether to use built in mappers (such as String to Integer) + */ + @Option.DefaultBoolean(true) + boolean useBuiltInMappers(); +} diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/MappersConfigSupport.java b/common/mapper/src/main/java/io/helidon/common/mapper/MappersConfigSupport.java new file mode 100644 index 00000000000..5cd072e5733 --- /dev/null +++ b/common/mapper/src/main/java/io/helidon/common/mapper/MappersConfigSupport.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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.helidon.common.mapper; + +import java.util.List; +import java.util.Set; + +import io.helidon.builder.api.Prototype; +import io.helidon.common.GenericType; +import io.helidon.common.Weighted; +import io.helidon.common.Weights; +import io.helidon.common.mapper.spi.MapperProvider; + +final class MappersConfigSupport { + private MappersConfigSupport() { + } + + static final class CustomMethods { + private CustomMethods() { + } + + /** + * Add a mapper to the list of mapper. + * + * @param builder ignored + * @param mapper the mapper to map source instances to target instances + * @param sourceType class of the source instance + * @param targetType class of the target instance + * @param qualifiers supported qualifiers of this mapper (if none provided, will return a compatible response) + * @param type of source + * @param type of target + */ + @Prototype.BuilderMethod + static void addMapper(MappersConfig.BuilderBase builder, + Mapper mapper, + Class sourceType, + Class targetType, + String... qualifiers) { + addMapper(builder, mapper, sourceType, targetType, Weighted.DEFAULT_WEIGHT, qualifiers); + } + + /** + * Add a mapper to the list of mapper. + * + * @param builder ignored + * @param mapper the mapper to map source instances to target instances + * @param sourceType generic type of the source instance + * @param targetType generic type of the target instance + * @param qualifiers qualifiers of this mapper, if empty, will be a compatible mapper + * @param type of source + * @param type of target + */ + @Prototype.BuilderMethod + static void addMapper(MappersConfig.BuilderBase builder, + Mapper mapper, + GenericType sourceType, + GenericType targetType, + String... qualifiers) { + + addMapper(builder, mapper, sourceType, targetType, Weighted.DEFAULT_WEIGHT, qualifiers); + } + + /** + * Add a mapper to the list of mapper with a custom priority. + * + * @param builder ignored + * @param mapper the mapper to map source instances to target instances + * @param sourceType class of the source instance + * @param targetType class of the target instance + * @param weight weight of the mapper + * @param qualifiers supported qualifiers of this mapper (if none provided, will return a compatible response) + * @param type of source + * @param type of target + */ + @Prototype.BuilderMethod + static void addMapper(MappersConfig.BuilderBase builder, + Mapper mapper, + Class sourceType, + Class targetType, + double weight, + String... qualifiers) { + builder.addMapperProvider(new ClassMapperProvider(mapper, + sourceType, + targetType, + weight, + Set.of(qualifiers))); + } + + /** + * Add a mapper to the list of mapper with custom priority. + * + * @param builder ignored + * @param mapper the mapper to map source instances to target instances + * @param sourceType generic type of the source instance + * @param targetType generic type of the target instance + * @param weight weight of the mapper + * @param qualifiers qualifiers of this mapper, if empty, will be a compatible mapper + * @param type of source + * @param type of target + */ + @Prototype.BuilderMethod + static void addMapper(MappersConfig.BuilderBase builder, + Mapper mapper, + GenericType sourceType, + GenericType targetType, + double weight, + String... qualifiers) { + if (sourceType.isClass() && targetType.isClass()) { + builder.addMapperProvider(new ClassMapperProvider(mapper, + sourceType.rawType(), + targetType.rawType(), + weight, + Set.of(qualifiers))); + } else { + builder.addMapperProvider(new GenericMapperProvider(mapper, + sourceType, + targetType, + weight, + Set.of(qualifiers))); + } + } + } + + @SuppressWarnings("rawtypes") + private static final class GenericMapperProvider implements MapperProvider, Weighted { + + private final Mapper mapper; + private final GenericType sourceType; + private final GenericType targetType; + private final double weight; + private final Set qualifiers; + + private GenericMapperProvider(Mapper mapper, + GenericType sourceType, + GenericType targetType, + double weight, + Set qualifiers) { + this.mapper = mapper; + this.sourceType = sourceType; + this.targetType = targetType; + this.weight = weight; + this.qualifiers = qualifiers; + } + + @Override + public ProviderResponse mapper(Class sourceClass, Class targetClass, String qualifier) { + return ProviderResponse.unsupported(); + } + + @Override + public ProviderResponse mapper(GenericType sourceType, GenericType targetType, String qualifier) { + if ((sourceType.equals(this.sourceType)) && (targetType.equals(this.targetType))) { + if (this.qualifiers.contains(qualifier)) { + return new ProviderResponse(Support.SUPPORTED, mapper); + } else { + return new ProviderResponse(Support.COMPATIBLE, mapper); + } + + } + return ProviderResponse.unsupported(); + } + + @Override + public double weight() { + return weight; + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static final class ClassMapperProvider implements MapperProvider, Weighted { + private final Mapper mapper; + private final Class sourceType; + private final Class targetType; + private final double weight; + private final Set qualifiers; + + private ClassMapperProvider(Mapper mapper, + Class sourceType, + Class targetType, + double weight, + Set qualifiers) { + this.mapper = mapper; + this.sourceType = sourceType; + this.targetType = targetType; + this.weight = weight; + this.qualifiers = qualifiers; + } + + @Override + public ProviderResponse mapper(Class sourceClass, Class targetClass, String qualifier) { + if ((this.sourceType == sourceClass) && (this.targetType == targetClass)) { + if (this.qualifiers.contains(qualifier)) { + return new MapperProvider.ProviderResponse(MapperProvider.Support.SUPPORTED, mapper); + } else { + return new MapperProvider.ProviderResponse(MapperProvider.Support.COMPATIBLE, mapper); + } + } + return MapperProvider.ProviderResponse.unsupported(); + } + + @Override + public double weight() { + return weight; + } + } + + static class BuilderDecorator implements Prototype.BuilderDecorator> { + BuilderDecorator() { + } + + @Override + public void decorate(MappersConfig.BuilderBase target) { + if (target.useBuiltInMappers()) { + target.addMapperProvider(new BuiltInMappers()); + } + + List> mappers = target.mappers(); + for (Mapper mapper : mappers) { + target.addMapperProvider(new GenericMapperProvider(mapper, + mapper.sourceType(), + mapper.targetType(), + Weights.find(mapper, Weighted.DEFAULT_WEIGHT), + mapper.qualifiers())); + } + } + } +} diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/MappersFactory.java b/common/mapper/src/main/java/io/helidon/common/mapper/MappersFactory.java new file mode 100644 index 00000000000..42ed88c1b91 --- /dev/null +++ b/common/mapper/src/main/java/io/helidon/common/mapper/MappersFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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.helidon.common.mapper; + +import java.util.List; +import java.util.function.Supplier; + +import io.helidon.common.mapper.spi.MapperProvider; +import io.helidon.service.registry.Service; + +@Service.Singleton +class MappersFactory implements Supplier { + private final List mapperProviders; + private final List> mappers; + + @Service.Inject + MappersFactory(List mapperProviders, List> mappers) { + this.mapperProviders = mapperProviders; + this.mappers = mappers; + } + + @Override + public Mappers get() { + return Mappers.builder() + .mapperProvidersDiscoverServices(false) + .mappersDiscoverServices(false) + .update(it -> mappers.forEach(it::addMapper)) + .update(it -> mapperProviders.forEach(it::addMapperProvider)) + .build(); + } +} diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/MapperManagerImpl.java b/common/mapper/src/main/java/io/helidon/common/mapper/MappersImpl.java similarity index 91% rename from common/mapper/src/main/java/io/helidon/common/mapper/MapperManagerImpl.java rename to common/mapper/src/main/java/io/helidon/common/mapper/MappersImpl.java index b0e4b2b2752..d4ae35c37c7 100644 --- a/common/mapper/src/main/java/io/helidon/common/mapper/MapperManagerImpl.java +++ b/common/mapper/src/main/java/io/helidon/common/mapper/MappersImpl.java @@ -16,6 +16,7 @@ package io.helidon.common.mapper; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -24,17 +25,40 @@ import java.util.function.BiFunction; import io.helidon.common.GenericType; +import io.helidon.common.Weights; import io.helidon.common.mapper.spi.MapperProvider; +import io.helidon.service.registry.Service; /** - * Implementation of {@link io.helidon.common.mapper.MapperManager}. + * Implementation of {@link io.helidon.common.mapper.Mappers}. */ -final class MapperManagerImpl implements MapperManager { - private final List providers; +@SuppressWarnings("removal") +@Service.Singleton +final class MappersImpl implements MapperManager, Mappers { private final Map> classCache = new ConcurrentHashMap<>(); private final Map> typeCache = new ConcurrentHashMap<>(); - MapperManagerImpl(Builder builder) { + private final List providers; + private final MappersConfig prototype; + + @Service.Inject + MappersImpl() { + this(MappersConfig.create()); + } + + MappersImpl(MappersConfig prototype) { + this.prototype = prototype; + List providers = new ArrayList<>(prototype.mapperProviders()); + Weights.sort(providers); + this.providers = providers; + } + + @SuppressWarnings("removal") + MappersImpl(MapperManager.Builder builder) { + this.prototype = MappersConfig.builder() + .useBuiltInMappers(false) + .mapperProviders(builder.mapperProviders()) + .buildPrototype(); this.providers = builder.mapperProviders(); } @@ -76,6 +100,11 @@ public Optional> mapper(GenericType extends Value { * @param qualifiers qualifiers of the mapper * @return an empty value */ - static OptionalValue create(MapperManager mapperManager, String name, Class type, String... qualifiers) { + static OptionalValue create(Mappers mapperManager, String name, Class type, String... qualifiers) { Objects.requireNonNull(name, "Name of the Value must not be null"); return create(mapperManager, name, GenericType.create(type), qualifiers); } @@ -58,7 +58,7 @@ static OptionalValue create(MapperManager mapperManager, String name, Cla * @param type of the value * @return an empty value */ - static OptionalValue create(MapperManager mapperManager, String name, GenericType type, String... qualifiers) { + static OptionalValue create(Mappers mapperManager, String name, GenericType type, String... qualifiers) { Objects.requireNonNull(name, "Name of the Value must not be null"); return new ValueEmpty<>(mapperManager, type, name, qualifiers); } @@ -73,7 +73,7 @@ static OptionalValue create(MapperManager mapperManager, String name, Gen * @param type of the value * @return a value backed by data */ - static OptionalValue create(MapperManager mapperManager, String name, T value, String... qualifiers) { + static OptionalValue create(Mappers mapperManager, String name, T value, String... qualifiers) { Objects.requireNonNull(name, "Name of the Value must not be null"); Objects.requireNonNull(value, "Value content for Value " + name + " must not be null, use empty(String) instead"); return new ValueBacked<>(mapperManager, name, value, qualifiers); @@ -90,7 +90,7 @@ static OptionalValue create(MapperManager mapperManager, String name, T v * @param type of the value * @return a value backed by data */ - static OptionalValue create(MapperManager mapperManager, + static OptionalValue create(Mappers mapperManager, String name, T value, GenericType type, diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/Value.java b/common/mapper/src/main/java/io/helidon/common/mapper/Value.java index be7f63b6755..b9f47ca4f30 100644 --- a/common/mapper/src/main/java/io/helidon/common/mapper/Value.java +++ b/common/mapper/src/main/java/io/helidon/common/mapper/Value.java @@ -42,7 +42,7 @@ public interface Value { * @param type of the value * @return a value backed by data */ - static Value create(MapperManager mapperManager, String name, T value, String... qualifiers) { + static Value create(Mappers mapperManager, String name, T value, String... qualifiers) { Objects.requireNonNull(name, "Name of the Value must not be null"); Objects.requireNonNull(value, "Value content for Value " + name + " must not be null, use empty(String) instead"); return new ValueBacked<>(mapperManager, name, value, qualifiers); @@ -59,7 +59,7 @@ static Value create(MapperManager mapperManager, String name, T value, St * @param type of the value * @return a value backed by data */ - static Value create(MapperManager mapperManager, String name, T value, GenericType type, String... qualifiers) { + static Value create(Mappers mapperManager, String name, T value, GenericType type, String... qualifiers) { Objects.requireNonNull(name, "Name of the Value must not be null"); Objects.requireNonNull(value, "Value content for Value " + name + " must not be null, use empty(String) instead"); return new ValueBacked<>(mapperManager, name, value, type, qualifiers); diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/ValueBacked.java b/common/mapper/src/main/java/io/helidon/common/mapper/ValueBacked.java index 517f219fb5f..a9b37443eb1 100644 --- a/common/mapper/src/main/java/io/helidon/common/mapper/ValueBacked.java +++ b/common/mapper/src/main/java/io/helidon/common/mapper/ValueBacked.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,13 +23,13 @@ import io.helidon.common.GenericType; class ValueBacked implements OptionalValue { - private final MapperManager mapperManager; + private final Mappers mapperManager; private final String name; private final T value; private final GenericType type; private final String[] qualifiers; - ValueBacked(MapperManager mapperManager, String name, T value, GenericType type, String[] qualifiers) { + ValueBacked(Mappers mapperManager, String name, T value, GenericType type, String[] qualifiers) { Objects.requireNonNull(mapperManager); Objects.requireNonNull(name); Objects.requireNonNull(value); @@ -41,7 +41,7 @@ class ValueBacked implements OptionalValue { this.qualifiers = qualifiers; } - ValueBacked(MapperManager mapperManager, String name, T value, String[] qualifiers) { + ValueBacked(Mappers mapperManager, String name, T value, String[] qualifiers) { this(mapperManager, name, value, null, qualifiers); } diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/ValueEmpty.java b/common/mapper/src/main/java/io/helidon/common/mapper/ValueEmpty.java index c44578ebf0a..ae5b1a04fc2 100644 --- a/common/mapper/src/main/java/io/helidon/common/mapper/ValueEmpty.java +++ b/common/mapper/src/main/java/io/helidon/common/mapper/ValueEmpty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,12 +24,12 @@ import io.helidon.common.GenericType; class ValueEmpty implements OptionalValue { - private final MapperManager mapperManager; + private final Mappers mapperManager; private final GenericType type; private final String name; private final String[] qualifiers; - ValueEmpty(MapperManager mapperManager, GenericType type, String name, String[] qualifiers) { + ValueEmpty(Mappers mapperManager, GenericType type, String name, String[] qualifiers) { this.mapperManager = mapperManager; this.type = type; this.name = name; diff --git a/common/mapper/src/main/java/module-info.java b/common/mapper/src/main/java/module-info.java index 23f255af38b..ba4c4d6d0d8 100644 --- a/common/mapper/src/main/java/module-info.java +++ b/common/mapper/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,11 @@ */ module io.helidon.common.mapper { requires transitive io.helidon.common; + requires transitive io.helidon.builder.api; + + requires io.helidon.common.types; + + requires io.helidon.service.registry; exports io.helidon.common.mapper; exports io.helidon.common.mapper.spi; diff --git a/common/mapper/src/main/resources/META-INF/helidon/service.loader b/common/mapper/src/main/resources/META-INF/helidon/service.loader new file mode 100644 index 00000000000..9a2000c98d4 --- /dev/null +++ b/common/mapper/src/main/resources/META-INF/helidon/service.loader @@ -0,0 +1,2 @@ +# List of service contracts we want to support either from service registry, or from service loader +io.helidon.common.mapper.spi.MapperProvider diff --git a/common/mapper/src/test/java/io/helidon/common/mapper/CustomMapper.java b/common/mapper/src/test/java/io/helidon/common/mapper/CustomMapper.java new file mode 100644 index 00000000000..1025bac5c75 --- /dev/null +++ b/common/mapper/src/test/java/io/helidon/common/mapper/CustomMapper.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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.helidon.common.mapper; + +import io.helidon.common.GenericType; +import io.helidon.common.Size; +import io.helidon.service.registry.Service; + +@Service.Singleton +class CustomMapper implements Mapper { + @Override + public Size map(String string) { + return Size.parse(string); + } + + @Override + public GenericType sourceType() { + return GenericType.STRING; + } + + @Override + public GenericType targetType() { + return GenericType.create(Size.class); + } +} diff --git a/common/mapper/src/test/java/io/helidon/common/mapper/MapperManagerTest.java b/common/mapper/src/test/java/io/helidon/common/mapper/MapperManagerTestDeprecated.java similarity index 87% rename from common/mapper/src/test/java/io/helidon/common/mapper/MapperManagerTest.java rename to common/mapper/src/test/java/io/helidon/common/mapper/MapperManagerTestDeprecated.java index 06bfe86b075..373df299092 100644 --- a/common/mapper/src/test/java/io/helidon/common/mapper/MapperManagerTest.java +++ b/common/mapper/src/test/java/io/helidon/common/mapper/MapperManagerTestDeprecated.java @@ -31,7 +31,8 @@ /** * Unit test for {@link MapperManager}. */ -class MapperManagerTest { +@SuppressWarnings("removal") +class MapperManagerTestDeprecated { @Test void testUsingServiceLoader() { MapperManager mm = MapperManager.create(); @@ -42,11 +43,11 @@ void testUsingServiceLoader() { assertThat(result, is(10)); // using generic types - result = mm.map(source, ServiceLoaderMapper2.STRING_TYPE, ServiceLoaderMapper2.INTEGER_TYPE, "default"); + result = mm.map(source, ServiceRegistryMapper.STRING_TYPE, ServiceRegistryMapper.INTEGER_TYPE, "default"); assertThat(result, is(11)); // search for opposite (use class, find type and vice versa) - Long longResult = mm.map(source, ServiceLoaderMapper2.STRING_TYPE, GenericType.create(Long.class), "default"); + Long longResult = mm.map(source, ServiceRegistryMapper.STRING_TYPE, GenericType.create(Long.class), "default"); assertThat(longResult, is(10L)); // must be the same longResult = mm.map(source, String.class, Long.class, "default"); @@ -55,7 +56,7 @@ void testUsingServiceLoader() { Short shortResult = mm.map(source, String.class, Short.class, "default"); assertThat(shortResult, is((short) 10)); // must be the same - shortResult = mm.map(source, ServiceLoaderMapper2.STRING_TYPE, ServiceLoaderMapper2.SHORT_TYPE, "default"); + shortResult = mm.map(source, ServiceRegistryMapper.STRING_TYPE, ServiceRegistryMapper.SHORT_TYPE, "default"); assertThat(shortResult, is((short) 10)); assertThrows(MapperException.class, () -> mm.map(source, String.class, Object.class, "default")); @@ -65,7 +66,7 @@ void testUsingServiceLoader() { void testUsingCustomProviders() { MapperManager mm = MapperManager.builder() .discoverServices(false) - .addMapperProvider(new ServiceLoaderMapper1()) + .addMapperProvider(new ServiceLoaderMapper()) .build(); String source = "10"; @@ -74,19 +75,19 @@ void testUsingCustomProviders() { assertThat(result, is(10)); // using generic types - result = mm.map(source, ServiceLoaderMapper2.STRING_TYPE, ServiceLoaderMapper2.INTEGER_TYPE, "default"); + result = mm.map(source, ServiceRegistryMapper.STRING_TYPE, ServiceRegistryMapper.INTEGER_TYPE, "default"); assertThat(result, is(10)); // search for opposite (use class, find type and vice versa) - Long longResult = mm.map(source, ServiceLoaderMapper2.STRING_TYPE, GenericType.create(Long.class), "default"); + Long longResult = mm.map(source, ServiceRegistryMapper.STRING_TYPE, GenericType.create(Long.class), "default"); assertThat(longResult, is(10L)); // must be the same longResult = mm.map(source, String.class, Long.class, "default"); assertThat(longResult, is(10L)); assertThrows(MapperException.class, () -> mm.map(source, String.class, Short.class, "default")); - assertThrows(MapperException.class, () -> mm.map(source, ServiceLoaderMapper2.STRING_TYPE, - ServiceLoaderMapper2.SHORT_TYPE, "default")); + assertThrows(MapperException.class, () -> mm.map(source, ServiceRegistryMapper.STRING_TYPE, + ServiceRegistryMapper.SHORT_TYPE, "default")); assertThrows(MapperException.class, () -> mm.map(source, String.class, Object.class, "default")); } @@ -94,7 +95,7 @@ void testUsingCustomProviders() { void testUsingServiceLoaderAndCustomMappers() { MapperManager mm = MapperManager.builder() .addMapper(String::valueOf, Integer.class, String.class) - .addMapper(String::valueOf, ServiceLoaderMapper2.SHORT_TYPE, ServiceLoaderMapper2.STRING_TYPE) + .addMapper(String::valueOf, ServiceRegistryMapper.SHORT_TYPE, ServiceRegistryMapper.STRING_TYPE) .build(); String source = "10"; @@ -103,11 +104,11 @@ void testUsingServiceLoaderAndCustomMappers() { assertThat(result, is(10)); // using generic types - result = mm.map(source, ServiceLoaderMapper2.STRING_TYPE, ServiceLoaderMapper2.INTEGER_TYPE, "default"); + result = mm.map(source, ServiceRegistryMapper.STRING_TYPE, ServiceRegistryMapper.INTEGER_TYPE, "default"); assertThat(result, is(11)); // search for opposite (use class, find type and vice versa) - Long longResult = mm.map(source, ServiceLoaderMapper2.STRING_TYPE, GenericType.create(Long.class), "default"); + Long longResult = mm.map(source, ServiceRegistryMapper.STRING_TYPE, GenericType.create(Long.class), "default"); assertThat(longResult, is(10L)); // must be the same longResult = mm.map(source, String.class, Long.class, "default"); @@ -116,7 +117,7 @@ void testUsingServiceLoaderAndCustomMappers() { Short shortResult = mm.map(source, String.class, Short.class, "default"); assertThat(shortResult, is((short) 10)); // must be the same - shortResult = mm.map(source, ServiceLoaderMapper2.STRING_TYPE, ServiceLoaderMapper2.SHORT_TYPE, "default"); + shortResult = mm.map(source, ServiceRegistryMapper.STRING_TYPE, ServiceRegistryMapper.SHORT_TYPE, "default"); assertThat(shortResult, is((short) 10)); assertThrows(MapperException.class, () -> mm.map(source, String.class, Object.class, "default")); @@ -124,12 +125,12 @@ void testUsingServiceLoaderAndCustomMappers() { // and add tests for integer and short types String stringResult = mm.map(42, Integer.class, String.class, "default"); assertThat(stringResult, is("42")); - stringResult = mm.map(42, GenericType.create(Integer.class), ServiceLoaderMapper2.STRING_TYPE, "default"); + stringResult = mm.map(42, GenericType.create(Integer.class), ServiceRegistryMapper.STRING_TYPE, "default"); assertThat(stringResult, is("42")); stringResult = mm.map((short) 42, Short.class, String.class, "default"); assertThat(stringResult, is("42")); - stringResult = mm.map((short) 42, ServiceLoaderMapper2.SHORT_TYPE, ServiceLoaderMapper2.STRING_TYPE, "default"); + stringResult = mm.map((short) 42, ServiceRegistryMapper.SHORT_TYPE, ServiceRegistryMapper.STRING_TYPE, "default"); assertThat(stringResult, is("42")); } @@ -246,7 +247,7 @@ void testEmptyValue() { @Test void testCacheWorks() { - MapperManagerImpl mm = new MapperManagerImpl(MapperManager.builder() + MappersImpl mm = new MappersImpl(MapperManager.builder() .addMapperProvider(new TestProvider())); assertThat(mm.classCacheSize(), is(0)); assertThat(mm.typeCacheSize(), is(0)); diff --git a/common/mapper/src/test/java/io/helidon/common/mapper/MappersTest.java b/common/mapper/src/test/java/io/helidon/common/mapper/MappersTest.java new file mode 100644 index 00000000000..403a65b7c5b --- /dev/null +++ b/common/mapper/src/test/java/io/helidon/common/mapper/MappersTest.java @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * 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.helidon.common.mapper; + +import java.util.List; +import java.util.NoSuchElementException; + +import io.helidon.common.GenericType; +import io.helidon.common.Size; +import io.helidon.common.mapper.spi.MapperProvider; +import io.helidon.common.mapper.spi.MapperProvider.ProviderResponse; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Unit test for {@link io.helidon.common.mapper.Mappers}. + */ +class MappersTest { + @Test + void testUsingServiceLoader() { + Mappers mm = Mappers.create(); + + String source = "10"; + // using classes + Integer result = mm.map(source, String.class, Integer.class, "default"); + assertThat(result, is(10)); + + // uses the highest weight - ServiceRegistryMapper + result = mm.map(source, ServiceRegistryMapper.STRING_TYPE, ServiceRegistryMapper.INTEGER_TYPE, "default"); + assertThat(result, is(11)); + + // search for opposite (use class, find type and vice versa) + Long longResult = mm.map(source, ServiceRegistryMapper.STRING_TYPE, GenericType.create(Long.class), "default"); + assertThat(longResult, is(10L)); + // must be the same + longResult = mm.map(source, String.class, Long.class, "default"); + assertThat(longResult, is(10L)); + + Short shortResult = mm.map(source, String.class, Short.class, "default"); + assertThat(shortResult, is((short) 10)); + // must be the same + shortResult = mm.map(source, ServiceRegistryMapper.STRING_TYPE, ServiceRegistryMapper.SHORT_TYPE, "default"); + assertThat(shortResult, is((short) 10)); + + assertThrows(MapperException.class, () -> mm.map(source, String.class, Object.class, "default")); + } + + @Test + void testUsingCustomProviders() { + Mappers mm = Mappers.builder() + .mapperProvidersDiscoverServices(false) + .mapperProviders(List.of(new ServiceLoaderMapper())) + .useBuiltInMappers(false) + .build(); + + String source = "10"; + // using classes + Integer result = mm.map(source, String.class, Integer.class, "default"); + assertThat(result, is(10)); + + // using generic types + result = mm.map(source, ServiceRegistryMapper.STRING_TYPE, ServiceRegistryMapper.INTEGER_TYPE, "default"); + assertThat(result, is(10)); + + // search for opposite (use class, find type and vice versa) + Long longResult = mm.map(source, ServiceRegistryMapper.STRING_TYPE, GenericType.create(Long.class), "default"); + assertThat(longResult, is(10L)); + // must be the same + longResult = mm.map(source, String.class, Long.class, "default"); + assertThat(longResult, is(10L)); + + assertThrows(MapperException.class, () -> mm.map(source, String.class, Short.class, "default")); + assertThrows(MapperException.class, () -> mm.map(source, ServiceRegistryMapper.STRING_TYPE, + ServiceRegistryMapper.SHORT_TYPE, "default")); + assertThrows(MapperException.class, () -> mm.map(source, String.class, Object.class, "default")); + } + + @Test + void testUsingServiceLoaderAndCustomMappers() { + Mappers mm = Mappers.builder() + .addMapper(String::valueOf, Integer.class, String.class) + .addMapper(String::valueOf, ServiceRegistryMapper.SHORT_TYPE, ServiceRegistryMapper.STRING_TYPE) + .build(); + + String source = "10"; + // using classes + Integer result = mm.map(source, String.class, Integer.class, "default"); + assertThat(result, is(10)); + + // using generic types + // uses the highest weight - the ServiceRegistryMapper + result = mm.map(source, ServiceRegistryMapper.STRING_TYPE, ServiceRegistryMapper.INTEGER_TYPE, "default"); + assertThat(result, is(11)); + + // search for opposite (use class, find type and vice versa) + Long longResult = mm.map(source, ServiceRegistryMapper.STRING_TYPE, GenericType.create(Long.class), "default"); + assertThat(longResult, is(10L)); + // must be the same + longResult = mm.map(source, String.class, Long.class, "default"); + assertThat(longResult, is(10L)); + + Short shortResult = mm.map(source, String.class, Short.class, "default"); + assertThat(shortResult, is((short) 10)); + // must be the same + shortResult = mm.map(source, ServiceRegistryMapper.STRING_TYPE, ServiceRegistryMapper.SHORT_TYPE, "default"); + assertThat(shortResult, is((short) 10)); + + assertThrows(MapperException.class, () -> mm.map(source, String.class, Object.class, "default")); + + // and add tests for integer and short types + String stringResult = mm.map(42, Integer.class, String.class, "default"); + assertThat(stringResult, is("42")); + stringResult = mm.map(42, GenericType.create(Integer.class), ServiceRegistryMapper.STRING_TYPE, "default"); + assertThat(stringResult, is("42")); + + stringResult = mm.map((short) 42, Short.class, String.class, "default"); + assertThat(stringResult, is("42")); + stringResult = mm.map((short) 42, ServiceRegistryMapper.SHORT_TYPE, ServiceRegistryMapper.STRING_TYPE, "default"); + assertThat(stringResult, is("42")); + } + + @Test + void testQualifiedMapping() { + Mappers mapperManager = Mappers.builder() + .addMapperProvider((t1, t2, qualifier) -> { + if (qualifier.equals("http")) { + return new ProviderResponse(MapperProvider.Support.SUPPORTED, req -> "http_" + req); + } + return ProviderResponse.unsupported(); + }) + .addMapperProvider((t1, t2, qualifier) -> { + if (qualifier.equals("http/header")) { + return new ProviderResponse(MapperProvider.Support.SUPPORTED, req -> "http_header_" + req); + } + return ProviderResponse.unsupported(); + }) + .addMapperProvider((t1, t2, qualifier) -> { + if (qualifier.equals("http/query")) { + return new ProviderResponse(MapperProvider.Support.SUPPORTED, req -> "http_query_" + req); + } + return ProviderResponse.unsupported(); + }) + .useBuiltInMappers(false) + .build(); + + assertThrows(MapperException.class, () -> mapperManager.map("value", String.class, String.class, "")); + + // http qualifier exists + String value = mapperManager.map("value", String.class, String.class, "http"); + assertThat(value, is("http_value")); + + // http/header qualifier exist + value = mapperManager.map("value", String.class, String.class, "http", "header"); + assertThat(value, is("http_header_value")); + + // http/query qualifier exists + value = mapperManager.map("value", String.class, String.class, "http", "query"); + assertThat(value, is("http_query_value")); + + // should fall back to http qualifier + value = mapperManager.map("value", String.class, String.class, "http", "matrix"); + assertThat(value, is("http_value")); + } + + @Test + void testCustomMapper() { + // make sure the custom mapper is discovered and maps values + Mappers mappers = Mappers.create(); + Size result = mappers.map("12 KB", String.class, Size.class); + + assertThat(result, is(Size.create(12, Size.Unit.KIB))); + } + + @Test + void testExistingValue() { + // int -> double + // not double to int + + Mappers mapperManager = Mappers.builder() + .useBuiltInMappers(true) + .mapperProvidersDiscoverServices(false) + .addMapperProvider((t1, t2, qualifier) -> { + if (t1.equals(Integer.class) && t2.equals(Double.class)) { + return new ProviderResponse(MapperProvider.Support.SUPPORTED, anInt -> ((Integer) anInt).doubleValue()); + } + return ProviderResponse.unsupported(); + }) + .build(); + + Value value = Value.create(mapperManager, "name", "42"); + assertThat(value.get(), is("42")); + + Value integerValue = value.as(Integer.class); + assertThat(integerValue.get(), is(42)); + + value = integerValue.asString(); + assertThat(value.get(), is("42")); + + Value doubleValue = integerValue.as(Double.class); + assertThat(doubleValue.get(), is(42D)); + + assertThrows(MapperException.class, () -> doubleValue.as(Integer.class)); + } + + @Test + void testEmptyValue() { + // int -> double + // not double to int + + Mappers mapperManager = Mappers.builder() + .useBuiltInMappers(true) + .addMapperProvider((t1, t2, qualifier) -> { + if (t1.equals(Integer.class) && t2.equals(Double.class)) { + return new ProviderResponse(MapperProvider.Support.SUPPORTED, anInt -> ((Integer) anInt).doubleValue()); + } + return ProviderResponse.unsupported(); + }) + .build(); + + OptionalValue value = OptionalValue.create(mapperManager, "name", String.class); + assertThrows(NoSuchElementException.class, value::get); + assertThat(value.isPresent(), is(false)); + assertThat(value.isEmpty(), is(true)); + + OptionalValue integerValue = value.as(Integer.class); + assertThrows(NoSuchElementException.class, integerValue::get); + assertThat(integerValue.isPresent(), is(false)); + assertThat(integerValue.isEmpty(), is(true)); + + value = integerValue.asString(); + assertThrows(NoSuchElementException.class, value::get); + assertThat(value.isPresent(), is(false)); + assertThat(value.isEmpty(), is(true)); + + OptionalValue doubleValue = integerValue.as(Double.class); + assertThrows(NoSuchElementException.class, doubleValue::get); + assertThat(doubleValue.isPresent(), is(false)); + assertThat(doubleValue.isEmpty(), is(true)); + + assertThrows(MapperException.class, () -> doubleValue.as(Integer.class)); + } + + @Test + void testCacheWorks() { + MappersImpl mm = new MappersImpl(Mappers.builder() + .addMapperProvider(new TestProvider()) + .buildPrototype()); + assertThat(mm.classCacheSize(), is(0)); + assertThat(mm.typeCacheSize(), is(0)); + + mm.map("value", String.class, String.class, "value"); + mm.map("value", String.class, String.class, "value"); + mm.map("value", String.class, String.class, "value"); + assertThat(mm.classCacheSize(), is(1)); + + mm.map("value", GenericType.STRING, GenericType.STRING, "value"); + mm.map("value", GenericType.STRING, GenericType.STRING, "value"); + mm.map("value", GenericType.STRING, GenericType.STRING, "value"); + assertThat(mm.typeCacheSize(), is(1)); + } + + private static class TestProvider implements MapperProvider { + @Override + public ProviderResponse mapper(Class sourceClass, Class targetClass, String qualifier) { + return new ProviderResponse(Support.SUPPORTED, req -> req); + } + + @Override + public ProviderResponse mapper(GenericType sourceType, GenericType targetType, String qualifier) { + return MapperProvider.super.mapper(sourceType, targetType, qualifier); + } + } +} \ No newline at end of file diff --git a/common/mapper/src/test/java/io/helidon/common/mapper/ServiceLoaderMapper1.java b/common/mapper/src/test/java/io/helidon/common/mapper/ServiceLoaderMapper.java similarity index 91% rename from common/mapper/src/test/java/io/helidon/common/mapper/ServiceLoaderMapper1.java rename to common/mapper/src/test/java/io/helidon/common/mapper/ServiceLoaderMapper.java index c5d5a68bfbd..1980c296820 100644 --- a/common/mapper/src/test/java/io/helidon/common/mapper/ServiceLoaderMapper1.java +++ b/common/mapper/src/test/java/io/helidon/common/mapper/ServiceLoaderMapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ /** * Maps String to Integer and String to Long using class. */ -public class ServiceLoaderMapper1 implements MapperProvider { +public class ServiceLoaderMapper implements MapperProvider { @Override public ProviderResponse mapper(Class sourceClass, Class targetClass, String qualifier) { if ((sourceClass.equals(String.class)) && (targetClass.equals(Integer.class))) { diff --git a/common/mapper/src/test/java/io/helidon/common/mapper/ServiceLoaderMapper2.java b/common/mapper/src/test/java/io/helidon/common/mapper/ServiceRegistryMapper.java similarity index 86% rename from common/mapper/src/test/java/io/helidon/common/mapper/ServiceLoaderMapper2.java rename to common/mapper/src/test/java/io/helidon/common/mapper/ServiceRegistryMapper.java index 28aae543dba..24efebc87bd 100644 --- a/common/mapper/src/test/java/io/helidon/common/mapper/ServiceLoaderMapper2.java +++ b/common/mapper/src/test/java/io/helidon/common/mapper/ServiceRegistryMapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,17 @@ package io.helidon.common.mapper; import io.helidon.common.GenericType; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; import io.helidon.common.mapper.spi.MapperProvider; +import io.helidon.service.registry.Service; /** * Maps String to Integer, and String to Short using type. */ -public class ServiceLoaderMapper2 implements MapperProvider { +@Weight(Weighted.DEFAULT_WEIGHT + 1) +@Service.Singleton +class ServiceRegistryMapper implements MapperProvider { static final GenericType STRING_TYPE = GenericType.create(String.class); static final GenericType INTEGER_TYPE = GenericType.create(Integer.class); static final GenericType SHORT_TYPE = GenericType.create(Short.class); diff --git a/common/mapper/src/test/resources/META-INF/services/io.helidon.common.mapper.spi.MapperProvider b/common/mapper/src/test/resources/META-INF/services/io.helidon.common.mapper.spi.MapperProvider index fe23bb48cb4..6ccdb059429 100644 --- a/common/mapper/src/test/resources/META-INF/services/io.helidon.common.mapper.spi.MapperProvider +++ b/common/mapper/src/test/resources/META-INF/services/io.helidon.common.mapper.spi.MapperProvider @@ -14,5 +14,4 @@ # limitations under the License. # -io.helidon.common.mapper.ServiceLoaderMapper1 -io.helidon.common.mapper.ServiceLoaderMapper2 \ No newline at end of file +io.helidon.common.mapper.ServiceLoaderMapper diff --git a/common/types/src/main/java/io/helidon/common/types/ElementSignature.java b/common/types/src/main/java/io/helidon/common/types/ElementSignature.java index a167313516d..6e045d1c970 100644 --- a/common/types/src/main/java/io/helidon/common/types/ElementSignature.java +++ b/common/types/src/main/java/io/helidon/common/types/ElementSignature.java @@ -45,6 +45,39 @@ public sealed interface ElementSignature permits ElementSignatures.FieldSignatur ElementSignatures.MethodSignature, ElementSignatures.ParameterSignature, ElementSignatures.NoSignature { + /** + * A field signature. + * + * @param type type of the field + * @param name name of the field + * @return a new field signature + */ + static ElementSignature createField(TypeName type, String name) { + return ElementSignatures.createField(type, name); + } + + /** + * A constructor signature. + * + * @param parameters list of types of parameters + * @return a new constructor signature + */ + static ElementSignature createConstructor(List parameters) { + return ElementSignatures.createConstructor(parameters); + } + + /** + * A method signature. + * + * @param returnType return type of the method + * @param name name of the method + * @param parameters parameter types of the method + * @return a new method signature + */ + static ElementSignature createMethod(TypeName returnType, String name, List parameters) { + return ElementSignatures.createMethod(returnType, name, parameters); + } + /** * Type of the element. Resolves as follows: *