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-pluginio.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.commonhelidon-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:
+ *
+ *
A single instance - if the instance is configured on the builder by hand, registry is not used
+ *
An {@link java.util.Optional} instance - ditto
+ *
A {@link java.util.List} of instances - instances configured on the builder are combined with instances
+ * discovered in the registry; there is a generated method that allows for disabling registry use for each
+ * service
+ *
+ *
+ * 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:
*
diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedType.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedType.java
index e962800d60a..f78594ff1f0 100644
--- a/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedType.java
+++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedType.java
@@ -64,4 +64,11 @@ static ResolvedType create(TypeName typeName) {
* @return the type name this resolved type represents
*/
TypeName type();
+
+ /**
+ * The resolved name including all type arguments.
+ *
+ * @return fully qualified class name with all type arguments
+ */
+ String resolvedName();
}
diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java
index 24c73cd5fc6..eddfb6a7d0d 100644
--- a/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java
+++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/ResolvedTypeImpl.java
@@ -32,6 +32,11 @@ public TypeName type() {
return typeName;
}
+ @Override
+ public String resolvedName() {
+ return resolvedName;
+ }
+
@Override
public int hashCode() {
return noTypes ? typeName.hashCode() : resolvedName.hashCode();
diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameSupport.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameSupport.java
index 7c87d4eaced..791acb5f485 100644
--- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameSupport.java
+++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNameSupport.java
@@ -18,6 +18,7 @@
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
+import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -192,6 +193,11 @@ static void type(TypeName.BuilderBase, ?> builder, Type type) {
}
return;
}
+ if (reflectGenericType instanceof WildcardType) {
+ builder.className("?");
+ builder.wildcard(true);
+ return;
+ }
throw new IllegalArgumentException("We can only create a type from a class, GenericType, or a ParameterizedType,"
+ " but got: " + reflectGenericType.getClass().getName());
diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java
index caf18dbf866..b3d513c516e 100644
--- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java
+++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java
@@ -30,6 +30,7 @@
import io.helidon.common.Generated;
import io.helidon.common.GenericType;
+import io.helidon.common.Size;
/**
* Commonly used type names.
@@ -87,6 +88,13 @@ public final class TypeNames {
* Type name for {@link java.lang.annotation.Target}.
*/
public static final TypeName TARGET = TypeName.create(Target.class);
+ /**
+ * Wildcard type name, represented in code by {@code ?}.
+ */
+ public static final TypeName WILDCARD = TypeName.builder()
+ .className("?")
+ .wildcard(true)
+ .build();
/*
Primitive types and their boxed counterparts
@@ -199,6 +207,10 @@ public final class TypeNames {
* Helidon {@link io.helidon.common.GenericType}.
*/
public static final TypeName GENERIC_TYPE = TypeName.create(GenericType.class);
+ /**
+ * Type name for {@link io.helidon.common.Size}.
+ */
+ public static final TypeName SIZE = TypeName.create(Size.class);
private TypeNames() {
}
diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java
index 3de312eb13c..ff33c88c696 100644
--- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java
+++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java
@@ -188,4 +188,13 @@ default Object originatingElementValue() {
*/
@Option.Access("")
ElementSignature signature();
+
+ /**
+ * Type parameters of this element. Such as when a method is declared as {@code T generate(Class type)},
+ * this would return the generic type {@code T} with no upper or lower bounds.
+ *
+ * @return list of type parameters of this element
+ */
+ @Option.Singular
+ List typeParameters();
}
diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java
index 83ddf357e66..f30ca8fc3e7 100644
--- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java
+++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeFactory.java
@@ -243,6 +243,11 @@ private static Optional createTypeName(Set processed, Elem
return createTypeName(processed, ee.getReturnType());
}
+ if (type.getKind() == ElementKind.TYPE_PARAMETER) {
+ TypeMirror mirror = type.asType();
+ return createTypeName(processed, mirror);
+ }
+
List classNames = new ArrayList<>();
String simpleName = type.getSimpleName().toString();
diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java
index b5a42510ed8..1d54a0fec1c 100644
--- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java
+++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java
@@ -30,6 +30,7 @@
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationValue;
@@ -183,6 +184,7 @@ public static Optional createTypedElementInfoFromElement(AptCo
.map(Modifier::toString)
.collect(Collectors.toSet());
Set thrownChecked = Set.of();
+ List typeParameters = new ArrayList<>();
if (v instanceof ExecutableElement ee) {
typeMirror = Objects.requireNonNull(ee.getReturnType());
@@ -209,6 +211,17 @@ public static Optional createTypedElementInfoFromElement(AptCo
.filter(it -> isCheckedException(ctx, it))
.flatMap(it -> AptTypeFactory.createTypeName(it).stream())
.collect(Collectors.toSet());
+
+ var elementTypeParameters = ee.getTypeParameters();
+ if (!elementTypeParameters.isEmpty()) {
+ // we need to keep the formal order and number of type parameters; if we cannot create it, just use error
+ elementTypeParameters.stream()
+ .map(AptTypeFactory::createTypeName)
+ .flatMap(it -> it.isPresent()
+ ? it.stream()
+ : Stream.of(TypeName.createFromGenericDeclaration("error")))
+ .forEach(typeParameters::add);
+ }
} else if (v instanceof VariableElement ve) {
typeMirror = Objects.requireNonNull(ve.asType());
}
@@ -254,6 +267,7 @@ public static Optional createTypedElementInfoFromElement(AptCo
.accessModifier(accessModifier(modifierNames))
.throwsChecked(thrownChecked)
.parameterArguments(params)
+ .typeParameters(typeParameters)
.originatingElement(v);
// To be failure-tolerant, as the ECJ may not provide an enclosing element for a VariableElement.
@@ -697,7 +711,8 @@ private static boolean isTypeInThisModule(AptContext ctx,
moduleName.set(null);
ModuleElement module = ctx.aptEnv().getElementUtils().getModuleOf(type);
- if (!module.isUnnamed()) {
+
+ if (module != null && !module.isUnnamed()) {
String name = module.getQualifiedName().toString();
if (hasValue(name)) {
moduleName.set(name);
diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java
index f18cbbfffef..7dbde98365b 100644
--- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java
+++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/TypeArgument.java
@@ -53,6 +53,24 @@ public static TypeArgument create(String token) {
return builder().token(token).build();
}
+ /**
+ * Creates a new instance from a generic type name.
+ *
+ * @param typeName type name to use
+ * @return a new type argument
+ */
+ public static TypeArgument create(TypeName typeName) {
+ var builder = TypeArgument.builder()
+ .token(typeName.className());
+
+ typeName.upperBounds()
+ .forEach(it -> builder.bound(it).lowerBound(false));
+ typeName.lowerBounds()
+ .forEach(it -> builder.bound(it).lowerBound(true));
+
+ return builder.build();
+ }
+
/**
* Return new {@link Builder} instance.
*
diff --git a/codegen/compiler/src/main/java/io/helidon/codegen/compiler/CompilerOptionsBlueprint.java b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/CompilerOptionsBlueprint.java
index 37c17191603..243d60fa4c9 100644
--- a/codegen/compiler/src/main/java/io/helidon/codegen/compiler/CompilerOptionsBlueprint.java
+++ b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/CompilerOptionsBlueprint.java
@@ -18,6 +18,7 @@
import java.nio.file.Path;
import java.util.List;
+import java.util.Optional;
import io.helidon.builder.api.Option;
import io.helidon.builder.api.Prototype;
@@ -77,6 +78,14 @@ interface CompilerOptionsBlueprint {
@Option.Default("21")
String target();
+ /**
+ * The compiler release.
+ * If not specified, {@link #source()} and {@link #target()} would be used.
+ *
+ * @return release for compilation
+ */
+ Optional release();
+
/**
* Target directory to generate class files to.
*
diff --git a/codegen/compiler/src/main/java/io/helidon/codegen/compiler/JavaC.java b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/JavaC.java
index ac7f67127c8..ea116847ef9 100644
--- a/codegen/compiler/src/main/java/io/helidon/codegen/compiler/JavaC.java
+++ b/codegen/compiler/src/main/java/io/helidon/codegen/compiler/JavaC.java
@@ -44,6 +44,7 @@ class JavaC {
private final List commandLineArgs;
private final String source;
private final String target;
+ private final String release;
private final Path outputDirectory;
private final CodegenLogger logger;
@@ -54,6 +55,7 @@ private JavaC(CompilerOptions options) {
this.commandLineArgs = options.commandLineArguments();
this.source = options.source();
this.target = options.target();
+ this.release = options.release().orElse(null);
this.outputDirectory = options.outputDirectory();
this.logger = options.logger();
}
@@ -123,14 +125,20 @@ private void doCompile(Result result, Path[] sourceFilesToCompile) {
optionList.add("--source-path");
optionList.add(toSourcepath());
}
- if (source != null) {
- optionList.add("--source");
- optionList.add(source);
- }
- if (target != null) {
- optionList.add("--target");
- optionList.add(target);
+ if (release == null) {
+ if (source != null) {
+ optionList.add("--source");
+ optionList.add(source);
+ }
+ if (target != null) {
+ optionList.add("--target");
+ optionList.add(target);
+ }
+ } else {
+ optionList.add("--release");
+ optionList.add(release);
}
+
optionList.addAll(commandLineArgs);
if (outputDirectory != null) {
optionList.add("-d");
diff --git a/common/common/src/main/java/io/helidon/common/HelidonServiceLoader.java b/common/common/src/main/java/io/helidon/common/HelidonServiceLoader.java
index d31e4280839..3bafbc9aa44 100644
--- a/common/common/src/main/java/io/helidon/common/HelidonServiceLoader.java
+++ b/common/common/src/main/java/io/helidon/common/HelidonServiceLoader.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.
@@ -100,6 +100,19 @@ public static Builder builder(ServiceLoader serviceLoader) {
return new Builder<>(serviceLoader);
}
+ /**
+ * A shortcut method to create a service loader based on the provider interface directly.
+ *
+ * @param theProviderInterface provider interface
+ * @return service loader
+ * @param type of the service
+ */
+ public static HelidonServiceLoader create(Class theProviderInterface) {
+ HelidonServiceLoader.class.getModule()
+ .addUses(theProviderInterface);
+ return create(ServiceLoader.load(theProviderInterface));
+ }
+
/**
* Create a weighted service loader from a Java Service loader.
*
diff --git a/common/common/src/main/java/io/helidon/common/LruCache.java b/common/common/src/main/java/io/helidon/common/LruCache.java
new file mode 100644
index 00000000000..d1c3da6bf65
--- /dev/null
+++ b/common/common/src/main/java/io/helidon/common/LruCache.java
@@ -0,0 +1,115 @@
+/*
+ * 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;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/**
+ * Least recently used cache.
+ * This cache has a capacity. When the capacity is reached, the oldest record is removed from the cache when a new one
+ * is added.
+ *
+ * @param type of the keys of the map
+ * @param type of the values of the map
+ */
+public interface LruCache {
+ /**
+ * Default capacity of the cache: {@value}.
+ */
+ int DEFAULT_CAPACITY = 10000;
+
+ /**
+ * Create an instance with default capacity.
+ *
+ * @param key type
+ * @param value type
+ * @return a new cache instance
+ * @see #DEFAULT_CAPACITY
+ */
+ static LruCache create() {
+ return new LruCacheImpl<>(DEFAULT_CAPACITY);
+ }
+
+ /**
+ * Create an instance with custom capacity.
+ *
+ * @param capacity of the cache
+ * @param key type
+ * @param value type
+ * @return a new cache instance
+ */
+ static LruCache create(int capacity) {
+ return new LruCacheImpl<>(capacity);
+ }
+
+ /**
+ * Get a value from the cache.
+ *
+ * @param key key to retrieve
+ * @return value if present or empty
+ */
+ Optional get(K key);
+
+ /**
+ * Remove a value from the cache.
+ *
+ * @param key key of the record to remove
+ * @return the value that was mapped to the key, or empty if none was
+ */
+ Optional remove(K key);
+
+ /**
+ * Put a value to the cache.
+ *
+ * @param key key to add
+ * @param value value to add
+ * @return value that was already mapped or empty if the value was not mapped
+ */
+ Optional put(K key, V value);
+
+ /**
+ * Either return a cached value or compute it and cache it.
+ * In case this method is called in parallel for the same key, the value actually present in the map may be from
+ * any of the calls.
+ * This method always returns either the existing value from the map, or the value provided by the supplier. It
+ * never returns a result from another thread's supplier.
+ *
+ * @param key key to check/insert value for
+ * @param valueSupplier supplier called if the value is not yet cached, or is invalid
+ * @return current value from the cache, or computed value from the supplier
+ */
+ Optional computeValue(K key, Supplier> valueSupplier);
+
+ /**
+ * Current size of the map.
+ *
+ * @return number of records currently cached
+ */
+ int size();
+
+ /**
+ * Capacity of this cache.
+ *
+ * @return configured capacity of this cache
+ */
+ int capacity();
+
+ /**
+ * Clear all records in the cache.
+ */
+ void clear();
+}
diff --git a/common/common/src/main/java/io/helidon/common/LruCacheImpl.java b/common/common/src/main/java/io/helidon/common/LruCacheImpl.java
new file mode 100644
index 00000000000..7b174bd2448
--- /dev/null
+++ b/common/common/src/main/java/io/helidon/common/LruCacheImpl.java
@@ -0,0 +1,152 @@
+/*
+ * 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;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Optional;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Supplier;
+
+/**
+ * Least recently used cache.
+ * This cache has a capacity. When the capacity is reached, the oldest record is removed from the cache when a new one
+ * is added.
+ *
+ * @param type of the keys of the map
+ * @param type of the values of the map
+ */
+final class LruCacheImpl implements LruCache {
+ private final LinkedHashMap backingMap = new LinkedHashMap<>();
+ private final ReadWriteLock rwLock = new ReentrantReadWriteLock(true);
+ private final Lock readLock = rwLock.readLock();
+ private final Lock writeLock = rwLock.writeLock();
+
+ private final int capacity;
+
+ LruCacheImpl(int capacity) {
+ this.capacity = capacity;
+ }
+
+ @Override
+ public Optional get(K key) {
+ readLock.lock();
+
+ V value;
+ try {
+ value = backingMap.get(key);
+ } finally {
+ readLock.unlock();
+ }
+
+ if (null == value) {
+ return Optional.empty();
+ }
+
+ writeLock.lock();
+ try {
+ // make sure the value is the last in the map (I do ignore a race here, as it is not significant)
+ // if some other thread moved another record to the front, we just move ours before it
+
+ // TODO this hurts - we just need to move the key to the last position
+ // maybe this should be replaced with a list and a map?
+ value = backingMap.get(key);
+ if (null == value) {
+ return Optional.empty();
+ }
+ backingMap.remove(key);
+ backingMap.put(key, value);
+
+ return Optional.of(value);
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ @Override
+ public Optional remove(K key) {
+
+ writeLock.lock();
+ try {
+ return Optional.ofNullable(backingMap.remove(key));
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ @Override
+ public Optional put(K key, V value) {
+ writeLock.lock();
+ try {
+ V currentValue = backingMap.remove(key);
+ if (null == currentValue) {
+ // need to free space - we did not make the map smaller
+ if (backingMap.size() >= capacity) {
+ Iterator iterator = backingMap.values().iterator();
+ iterator.next();
+ iterator.remove();
+ }
+ }
+
+ backingMap.put(key, value);
+ return Optional.ofNullable(currentValue);
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ @Override
+ public Optional computeValue(K key, Supplier> valueSupplier) {
+ // get is properly synchronized
+ Optional currentValue = get(key);
+ if (currentValue.isPresent()) {
+ return currentValue;
+ }
+ Optional newValue = valueSupplier.get();
+ // put is also properly synchronized - nevertheless we may replace the value more then once
+ // if called from parallel threads
+ newValue.ifPresent(theValue -> put(key, theValue));
+
+ return newValue;
+ }
+
+ @Override
+ public int size() {
+ readLock.lock();
+ try {
+ return backingMap.size();
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ @Override
+ public int capacity() {
+ return capacity;
+ }
+
+ @Override
+ public void clear() {
+ writeLock.lock();
+ try {
+ backingMap.clear();
+ } finally {
+ writeLock.unlock();
+ }
+ }
+}
diff --git a/common/common/src/test/java/io/helidon/common/LruCacheTest.java b/common/common/src/test/java/io/helidon/common/LruCacheTest.java
new file mode 100644
index 00000000000..d1b56c96c00
--- /dev/null
+++ b/common/common/src/test/java/io/helidon/common/LruCacheTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Unit test for {@link io.helidon.common.LruCache}.
+ */
+class LruCacheTest {
+ @Test
+ void testCache() {
+ LruCache theCache = LruCache.create();
+ String value = "cached";
+ String key = "theKey";
+ String newValue = "not-cached";
+
+ Optional res = theCache.put(key, value);
+ assertThat(res, is(Optional.empty()));
+ res = theCache.get(key);
+ assertThat(res, is(Optional.of(value)));
+ res = theCache.computeValue(key, () -> Optional.of(newValue));
+ assertThat(res, is(Optional.of(value)));
+ res = theCache.remove(key);
+ assertThat(res, is(Optional.of(value)));
+ res = theCache.get(key);
+ assertThat(res, is(Optional.empty()));
+ }
+
+ @Test
+ void testCacheComputeValue() {
+ LruCache theCache = LruCache.create();
+ String value = "cached";
+ String key = "theKey";
+ String newValue = "not-cached";
+
+ Optional res = theCache.computeValue(key, () -> Optional.of(value));
+ assertThat(res, is(Optional.of(value)));
+ res = theCache.get(key);
+ assertThat(res, is(Optional.of(value)));
+ res = theCache.computeValue(key, () -> Optional.of(newValue));
+ assertThat(res, is(Optional.of(value)));
+ res = theCache.remove(key);
+ assertThat(res, is(Optional.of(value)));
+ res = theCache.get(key);
+ assertThat(res, is(Optional.empty()));
+ }
+
+ @Test
+ void testMaxCapacity() {
+ LruCache theCache = LruCache.create(10);
+ for (int i = 0; i < 10; i++) {
+ theCache.put(i, i);
+ }
+ for (int i = 0; i < 10; i++) {
+ Optional integer = theCache.get(i);
+ assertThat(integer, is(Optional.of(i)));
+ }
+ theCache.put(10, 10);
+ Optional res = theCache.get(0);
+ assertThat(res, is(Optional.empty()));
+ res = theCache.get(10);
+ assertThat(res, is(Optional.of(10)));
+ }
+
+ @Test
+ void testLruBehavior() {
+ LruCache theCache = LruCache.create(10);
+ for (int i = 0; i < 10; i++) {
+ // insert all
+ theCache.put(i, i);
+ }
+ for (int i = 0; i < 10; i++) {
+ // use them in ascending order
+ Optional integer = theCache.get(i);
+ assertThat(integer, is(Optional.of(i)));
+ }
+ // now use 0
+ Optional value = theCache.get(0);
+ assertThat(value, is(Optional.of(0)));
+
+ theCache.put(10, 10);
+
+ // 0 should be in
+ value = theCache.get(0);
+ assertThat(value, is(Optional.of(0)));
+
+ // 1 should not
+ value = theCache.get(1);
+ assertThat(value, is(Optional.empty()));
+
+ }
+}
diff --git a/common/config/pom.xml b/common/config/pom.xml
index b9238f1f0e9..5d99847286d 100644
--- a/common/config/pom.xml
+++ b/common/config/pom.xml
@@ -37,6 +37,14 @@
io.helidon.commonhelidon-common-mapper
+
+ io.helidon.builder
+ helidon-builder-api
+
+
+ io.helidon.service
+ helidon-service-registry
+ org.junit.jupiterjunit-jupiter-api
diff --git a/common/config/src/main/java/io/helidon/common/config/ConfigBuilderSupport.java b/common/config/src/main/java/io/helidon/common/config/ConfigBuilderSupport.java
new file mode 100644
index 00000000000..5158e8695f6
--- /dev/null
+++ b/common/config/src/main/java/io/helidon/common/config/ConfigBuilderSupport.java
@@ -0,0 +1,193 @@
+/*
+ * 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.config;
+
+import java.util.List;
+import java.util.Optional;
+
+import io.helidon.builder.api.Prototype;
+import io.helidon.common.HelidonServiceLoader;
+import io.helidon.service.registry.ServiceRegistry;
+
+/**
+ * Methods used from generated code in builders when
+ * {@link io.helidon.builder.api.Prototype.Configured} is used.
+ */
+public final class ConfigBuilderSupport {
+ private ConfigBuilderSupport() {
+ }
+
+ /**
+ * Used to discover services from {@link io.helidon.service.registry.ServiceRegistry} for builder options annotated
+ * with {@link io.helidon.builder.api.Option.Provider}, if the blueprint is annotated with
+ * {@link io.helidon.builder.api.Prototype.RegistrySupport}.
+ *
+ * @param config configuration of the option
+ * @param configKey configuration key associated with this option
+ * @param serviceRegistry service registry instance
+ * @param providerType type of the service provider (contract)
+ * @param configType type of the configuration
+ * @param allFromRegistry whether to use all services from the registry
+ * @param existingValues existing values that was explicitly configured by the user
+ * @param type of the service
+ * @return instances from the user augmented with instances from the registry
+ */
+ public static List discoverServices(Config config,
+ String configKey,
+ Optional serviceRegistry,
+ Class extends ConfiguredProvider> providerType,
+ Class configType,
+ boolean allFromRegistry,
+ List existingValues) {
+
+ return ProvidedUtil.discoverServices(config,
+ configKey,
+ serviceRegistry,
+ providerType,
+ configType,
+ allFromRegistry,
+ existingValues);
+ }
+
+ /**
+ * Used to discover service from {@link io.helidon.service.registry.ServiceRegistry} for builder options annotated
+ * with {@link io.helidon.builder.api.Option.Provider}, if the blueprint is annotated with
+ * {@link io.helidon.builder.api.Prototype.RegistrySupport}.
+ *
+ * @param config configuration of the option
+ * @param configKey configuration key associated with this option
+ * @param serviceRegistry service registry instance
+ * @param providerType type of the service provider (contract)
+ * @param configType type of the configuration
+ * @param discoverServices whether to discover services from registry
+ * @param existingValue existing value that was explicitly configured by the user
+ * @param type of the service
+ * @return an instance, if available in the registry, or if provided by the user (user's value wins)
+ */
+ public static Optional
+ discoverService(Config config,
+ String configKey,
+ Optional serviceRegistry,
+ Class extends ConfiguredProvider> providerType,
+ Class configType,
+ boolean discoverServices,
+ Optional existingValue) {
+
+ return ProvidedUtil.discoverService(config,
+ configKey,
+ serviceRegistry,
+ providerType,
+ configType,
+ discoverServices,
+ existingValue);
+ }
+
+
+ /**
+ * 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)
+ */
+ public static > 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")
+ public static > 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);
+ }
+
+ /**
+ * 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 Prototype.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);
+ }
+}
diff --git a/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java b/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java
index 387dfdc2a85..4b6d41079ed 100644
--- a/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java
+++ b/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java
@@ -19,12 +19,14 @@
import java.util.List;
import java.util.Objects;
import java.util.ServiceLoader;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import io.helidon.common.HelidonServiceLoader;
import io.helidon.common.LazyValue;
import io.helidon.common.config.spi.ConfigProvider;
+import io.helidon.service.registry.Services;
/**
* Global configuration can be set by a user before any Helidon code is invoked, to override default discovery
@@ -34,8 +36,16 @@
* configuration.
*
* You may still use custom instances of configuration when using configurable APIs directly.
+ * @deprecated Use {@link io.helidon.service.registry.Services#get(Class)}, or
+ * {@link io.helidon.service.registry.ServiceRegistry#get(Class)} if you have an instance ready to obtain the "global"
+ * configuration; in case you are writing a registry service, simply inject the config instance; use
+ * {@link io.helidon.service.registry.Services#set(Class, Object[])} to use a custom instance of configuration, just make sure
+ * it is registered before it is used the first time
*/
+@Deprecated(forRemoval = true, since = "4.2.0")
public final class GlobalConfig {
+ private static final System.Logger LOGGER = System.getLogger(GlobalConfig.class.getName());
+ private static final AtomicBoolean LOGGED_REGISTERED = new AtomicBoolean(false);
private static final Config EMPTY = Config.empty();
private static final LazyValue DEFAULT_CONFIG = LazyValue.create(() -> {
List providers = HelidonServiceLoader.create(ServiceLoader.load(ConfigProvider.class))
@@ -68,7 +78,9 @@ public static boolean configured() {
* @return Helidon shared configuration instance if configured, or an empty configuration if not
* @see #config(java.util.function.Supplier)
* @see #config(java.util.function.Supplier, boolean)
+ * @deprecated use {@link io.helidon.service.registry.Services#get(Class)} instead
*/
+ @Deprecated(forRemoval = true, since = "4.2.0")
public static Config config() {
return configured() ? CONFIG.get() : DEFAULT_CONFIG.get();
}
@@ -78,7 +90,9 @@ public static Config config() {
*
* @param config configuration supplier to use if config is not yet configured
* @return used global configuration instance
+ * @deprecated use {@link io.helidon.service.registry.Services#set(Class, Object[])} instead
*/
+ @Deprecated(forRemoval = true, since = "4.2.0")
public static Config config(Supplier config) {
return config(config, false);
}
@@ -89,14 +103,30 @@ public static Config config(Supplier config) {
* @param config configuration to use
* @param overwrite whether to overwrite an existing configured value
* @return current global config
+ * @deprecated use {@link io.helidon.service.registry.Services#get(Class)} instead
*/
+ @Deprecated(forRemoval = true, since = "4.2.0")
public static Config config(Supplier config, boolean overwrite) {
Objects.requireNonNull(config);
if (overwrite || !configured()) {
// there is a certain risk we may do this twice, if two components try to set global config in parallel.
// as the result was already unclear (as order matters), we do not need to be 100% thread safe here
- CONFIG.set(config.get());
+ Config configInstance = config.get();
+ CONFIG.set(configInstance);
+
+ try {
+ Services.set(Config.class, configInstance);
+ } catch (Exception e) {
+ if (LOGGED_REGISTERED.compareAndSet(false, true)) {
+ // only log this once
+ LOGGER.log(System.Logger.Level.WARNING,
+ "Attempting to set a config instance when it either was already "
+ + "set once, or it was already used by a component. "
+ + "This will not work in future versions of Helidon",
+ e);
+ }
+ }
}
return CONFIG.get();
}
diff --git a/builder/api/src/main/java/io/helidon/builder/api/ProvidedUtil.java b/common/config/src/main/java/io/helidon/common/config/ProvidedUtil.java
similarity index 59%
rename from builder/api/src/main/java/io/helidon/builder/api/ProvidedUtil.java
rename to common/config/src/main/java/io/helidon/common/config/ProvidedUtil.java
index 100c23eedd3..ad2f31b51d3 100644
--- a/builder/api/src/main/java/io/helidon/builder/api/ProvidedUtil.java
+++ b/common/config/src/main/java/io/helidon/common/config/ProvidedUtil.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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.helidon.builder.api;
+package io.helidon.common.config;
import java.util.ArrayList;
import java.util.HashMap;
@@ -25,11 +25,10 @@
import java.util.Optional;
import java.util.Set;
+import io.helidon.builder.api.Prototype;
import io.helidon.common.HelidonServiceLoader;
-import io.helidon.common.config.Config;
-import io.helidon.common.config.ConfigException;
-import io.helidon.common.config.ConfiguredProvider;
-import io.helidon.common.config.NamedService;
+import io.helidon.service.registry.ServiceRegistry;
+import io.helidon.service.registry.Services;
final class ProvidedUtil {
private static final System.Logger PROVIDER_LOGGER = System.getLogger(Prototype.class.getName() + ".provider");
@@ -175,6 +174,92 @@ static List discoverServices(Config config,
}
}
+ static List discoverServices(Config config,
+ String configKey,
+ Optional serviceRegistry,
+ Class extends ConfiguredProvider> providerType,
+ Class configType,
+ boolean allFromRegistry,
+ List existingValues) {
+
+ // type and name is a unique identification of a service - for services already defined on the builder
+ // do not add them from configuration (as this would duplicate service instances)
+ Set ignoredServices = new HashSet<>();
+ existingValues.forEach(it -> ignoredServices.add(new TypeAndName(it.type(), it.name())));
+
+ boolean discoverServices = config.get(configKey + "-discover-services").asBoolean().orElse(allFromRegistry);
+ Config providersConfig = config.get(configKey);
+
+ List configuredServices = new ArrayList<>();
+
+ // all child nodes of the current node
+ List serviceConfigList = providersConfig.asNodeList()
+ .orElseGet(List::of);
+ boolean isList = providersConfig.isList();
+
+ for (Config serviceConfig : serviceConfigList) {
+ configuredServices.add(configuredService(serviceConfig, isList));
+ }
+ RegistryWrap wrap = serviceRegistry.isPresent()
+ ? new RealRegistry(serviceRegistry.get())
+ : StaticAccessRegistry.INSTANCE;
+
+ // now we have all service configurations, we can start building up instances
+ if (providersConfig.isList()) {
+ // driven by order of declaration in config
+ return servicesFromList(wrap,
+ providerType,
+ configType,
+ configuredServices,
+ discoverServices,
+ ignoredServices);
+ } else {
+ // driven by service loader order
+ return servicesFromObject(providersConfig,
+ wrap,
+ providerType,
+ configType,
+ configuredServices,
+ discoverServices,
+ ignoredServices);
+ }
+ }
+
+ static Optional
+ discoverService(Config config,
+ String configKey,
+ Optional serviceRegistry,
+ Class extends ConfiguredProvider> providerType,
+ Class configType,
+ boolean discoverServices,
+ Optional existingValue) {
+
+ // there is an explicit configuration for this service, ignore configuration
+ if (existingValue.isPresent()) {
+ return Optional.empty();
+ }
+
+ // all child nodes of the current node
+ List serviceConfigList = config.get(configKey).asNodeList()
+ .orElseGet(List::of);
+
+ // if more than one is configured in config, fail
+ // if more than one exists in service loader, use the first one
+ if (serviceConfigList.size() > 1) {
+ throw new ConfigException("There can only be one provider configured for " + config.key());
+ }
+
+ List services = discoverServices(config,
+ configKey,
+ serviceRegistry,
+ providerType,
+ configType,
+ discoverServices,
+ List.of());
+
+ return services.isEmpty() ? Optional.empty() : Optional.of(services.getFirst());
+ }
+
private static List
servicesFromObject(Config providersConfig,
HelidonServiceLoader extends ConfiguredProvider> serviceLoader,
@@ -304,7 +389,7 @@ private static ConfiguredService configuredService(Config serviceConfig, boolean
"Service provider configuration defined as a list must have a single node that is the type, "
+ "with children containing the provider configuration. Failed on: " + serviceConfig.key());
}
- usedConfig = configs.get(0);
+ usedConfig = configs.getFirst();
name = usedConfig.name();
type = usedConfig.get(KEY_SERVICE_TYPE).asString().orElse(name);
enabled = usedConfig.get(KEY_SERVICE_ENABLED).asBoolean().orElse(enabled);
@@ -320,9 +405,152 @@ private static ConfiguredService configuredService(Config serviceConfig, boolean
return new ConfiguredService(new TypeAndName(type, name), serviceConfig, enabled);
}
+ private static List
+ servicesFromList(RegistryWrap serviceRegistry,
+ Class extends ConfiguredProvider> providerType,
+ Class configType,
+ List configuredServices,
+ boolean allFromServiceLoader,
+ Set ignoredServices) {
+ Map> allProvidersByType = new HashMap<>();
+ Map> unusedProvidersByType = new LinkedHashMap<>();
+
+ serviceRegistry.all(providerType)
+ .forEach(provider -> {
+ allProvidersByType.put(provider.configKey(), provider);
+ unusedProvidersByType.put(provider.configKey(), provider);
+ });
+
+ List result = new ArrayList<>();
+
+ // first add all configured
+ for (ConfiguredService service : configuredServices) {
+ TypeAndName typeAndName = service.typeAndName();
+ if (!ignoredServices.add(typeAndName)) {
+ unusedProvidersByType.remove(typeAndName.type());
+
+ if (PROVIDER_LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
+ PROVIDER_LOGGER.log(System.Logger.Level.DEBUG, "Service: " + typeAndName
+ + " is already added in builder, ignoring configured one.");
+ }
+
+ continue;
+ }
+ ConfiguredProvider provider = allProvidersByType.get(typeAndName.type());
+ if (provider == null) {
+ throw new ConfigException("Unknown provider configured. Expecting a provider with type \"" + typeAndName.type()
+ + "\", but only the following providers are supported: "
+ + allProvidersByType.keySet() + ", "
+ + "provider interface: " + providerType.getName()
+ + ", configured service: " + configType.getName());
+ }
+ unusedProvidersByType.remove(typeAndName.type());
+ if (service.enabled()) {
+ result.add(provider.create(service.serviceConfig(), typeAndName.name()));
+ }
+ }
+
+ // then (if desired) add the rest
+ if (allFromServiceLoader) {
+ unusedProvidersByType.forEach((type, provider) -> {
+ if (ignoredServices.add(new TypeAndName(type, type))) {
+ result.add(provider.create(Config.empty(), type));
+ }
+ });
+ }
+
+ return result;
+ }
+
+ private static List
+ servicesFromObject(Config providersConfig,
+ RegistryWrap serviceRegistry,
+ Class extends ConfiguredProvider> providerType,
+ Class configType,
+ List configuredServices,
+ boolean allFromServiceLoader,
+ Set ignoredServices) {
+ // order is determined by service loader
+ Set availableProviders = new HashSet<>();
+ Map allConfigs = new HashMap<>();
+ configuredServices.forEach(it -> allConfigs.put(it.typeAndName().type, it));
+ Set unusedConfigs = new HashSet<>(allConfigs.keySet());
+
+ List result = new ArrayList<>();
+
+ List extends ConfiguredProvider> all = serviceRegistry.all(providerType);
+ for (ConfiguredProvider provider : all) {
+ ConfiguredService configuredService = allConfigs.get(provider.configKey());
+ availableProviders.add(provider.configKey());
+ unusedConfigs.remove(provider.configKey());
+ if (configuredService == null) {
+ if (allFromServiceLoader) {
+ // even though the specific key does not exist, we want to have the real config tree, so we can get to the
+ // root of it
+ // when there is no configuration, the name defaults to the type
+ String type = provider.configKey();
+ if (ignoredServices.add(new TypeAndName(type, type))) {
+ result.add(provider.create(providersConfig.get(type), type));
+ } else {
+ if (PROVIDER_LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
+ PROVIDER_LOGGER.log(System.Logger.Level.DEBUG, "Service: " + new TypeAndName(type, type)
+ + " is already added in builder, ignoring configured one.");
+ }
+ }
+ }
+ } else {
+ if (configuredService.enabled()) {
+ if (ignoredServices.add(configuredService.typeAndName())) {
+ result.add(provider.create(configuredService.serviceConfig(),
+ configuredService.typeAndName().name()));
+ } else {
+ if (PROVIDER_LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
+ PROVIDER_LOGGER.log(System.Logger.Level.DEBUG, "Service: " + configuredService.typeAndName()
+ + " is already added in builder, ignoring configured one.");
+ }
+ }
+ }
+ }
+ }
+
+ if (!unusedConfigs.isEmpty()) {
+ throw new ConfigException("Unknown provider configured. Expected providers with types: " + unusedConfigs
+ + ", but only the following providers are supported: " + availableProviders
+ + ", provider interface: " + providerType.getName()
+ + ", configured service: " + configType.getName());
+ }
+ return result;
+ }
+
+ private interface RegistryWrap {
+ List all(Class type);
+ }
+
private record TypeAndName(String type, String name) {
}
private record ConfiguredService(TypeAndName typeAndName, Config serviceConfig, boolean enabled) {
}
+
+ private static final class StaticAccessRegistry implements RegistryWrap {
+ public static final StaticAccessRegistry INSTANCE = new StaticAccessRegistry();
+
+ @Override
+ public List all(Class type) {
+ return Services.all(type);
+ }
+ }
+
+ private static final class RealRegistry implements RegistryWrap {
+ private final ServiceRegistry serviceRegistry;
+
+ private RealRegistry(ServiceRegistry serviceRegistry) {
+ this.serviceRegistry = serviceRegistry;
+ }
+
+ @Override
+ public List all(Class type) {
+ return serviceRegistry.all(type);
+ }
+ }
}
diff --git a/common/config/src/main/java/module-info.java b/common/config/src/main/java/module-info.java
index 9e0842c930e..d5cb643f9e8 100644
--- a/common/config/src/main/java/module-info.java
+++ b/common/config/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,6 +21,7 @@
requires transitive io.helidon.common;
requires transitive io.helidon.common.mapper;
+ requires io.helidon.service.registry;
exports io.helidon.common.config;
exports io.helidon.common.config.spi;
diff --git a/common/configurable/etc/spotbugs/exclude.xml b/common/configurable/etc/spotbugs/exclude.xml
index 6c4faead676..0bf0414c552 100644
--- a/common/configurable/etc/spotbugs/exclude.xml
+++ b/common/configurable/etc/spotbugs/exclude.xml
@@ -1,7 +1,7 @@
+
+
+
diff --git a/common/configurable/src/main/java/io/helidon/common/configurable/LruCache.java b/common/configurable/src/main/java/io/helidon/common/configurable/LruCache.java
index d57f72f7bae..317ddd2c982 100644
--- a/common/configurable/src/main/java/io/helidon/common/configurable/LruCache.java
+++ b/common/configurable/src/main/java/io/helidon/common/configurable/LruCache.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.
@@ -15,12 +15,7 @@
*/
package io.helidon.common.configurable;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
import java.util.Optional;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -33,24 +28,17 @@
*
* @param type of the keys of the map
* @param type of the values of the map
+ * @deprecated kindly use {@link io.helidon.common.LruCache}, we are removing this from the configurable module, as cache has
+ * only a single option, and we need it from modules that do not use configuration
*/
@RuntimeType.PrototypedBy(LruCacheConfig.class)
-public final class LruCache implements RuntimeType.Api> {
- /**
- * Default capacity of the cache: {@value}.
- */
- public static final int DEFAULT_CAPACITY = 10000;
-
- private final LinkedHashMap backingMap = new LinkedHashMap<>();
- private final ReadWriteLock rwLock = new ReentrantReadWriteLock(true);
- private final Lock readLock = rwLock.readLock();
- private final Lock writeLock = rwLock.writeLock();
-
- private final int capacity;
+@Deprecated(forRemoval = true, since = "4.2.0")
+public final class LruCache implements io.helidon.common.LruCache, RuntimeType.Api> {
private final LruCacheConfig config;
+ private final io.helidon.common.LruCache delegate;
private LruCache(LruCacheConfig config) {
- this.capacity = config.capacity();
+ this.delegate = io.helidon.common.LruCache.create(config.capacity());
this.config = config;
}
@@ -81,9 +69,9 @@ public static LruCache create() {
* Create an instance with custom configuration.
*
* @param config configuration of LRU cache
+ * @param key type
+ * @param value type
* @return a new cache instance
- * @param key type
- * @param value type
*/
public static LruCache create(LruCacheConfig config) {
return new LruCache<>(config);
@@ -93,9 +81,9 @@ public static LruCache create(LruCacheConfig config) {
* Create an instance with custom configuration.
*
* @param consumer of custom configuration builder
+ * @param key type
+ * @param value type
* @return a new cache instance
- * @param key type
- * @param value type
*/
public static LruCache create(Consumer> consumer) {
LruCacheConfig.Builder builder = LruCacheConfig.builder();
@@ -108,151 +96,38 @@ public LruCacheConfig prototype() {
return config;
}
- /**
- * Get a value from the cache.
- *
- * @param key key to retrieve
- * @return value if present or empty
- */
+ @Override
public Optional get(K key) {
- readLock.lock();
-
- V value;
- try {
- value = backingMap.get(key);
- } finally {
- readLock.unlock();
- }
-
- if (null == value) {
- return Optional.empty();
- }
-
- writeLock.lock();
- try {
- // make sure the value is the last in the map (I do ignore a race here, as it is not significant)
- // if some other thread moved another record to the front, we just move ours before it
-
- // TODO this hurts - we just need to move the key to the last position
- // maybe this should be replaced with a list and a map?
- value = backingMap.get(key);
- if (null == value) {
- return Optional.empty();
- }
- backingMap.remove(key);
- backingMap.put(key, value);
-
- return Optional.of(value);
- } finally {
- writeLock.unlock();
- }
+ return delegate.get(key);
}
- /**
- * Remove a value from the cache.
- *
- * @param key key of the record to remove
- * @return the value that was mapped to the key, or empty if none was
- */
+ @Override
public Optional remove(K key) {
-
- writeLock.lock();
- try {
- return Optional.ofNullable(backingMap.remove(key));
- } finally {
- writeLock.unlock();
- }
+ return delegate.remove(key);
}
- /**
- * Put a value to the cache.
- *
- * @param key key to add
- * @param value value to add
- * @return value that was already mapped or empty if the value was not mapped
- */
+ @Override
public Optional put(K key, V value) {
- writeLock.lock();
- try {
- V currentValue = backingMap.remove(key);
- if (null == currentValue) {
- // need to free space - we did not make the map smaller
- if (backingMap.size() >= capacity) {
- Iterator iterator = backingMap.values().iterator();
- iterator.next();
- iterator.remove();
- }
- }
-
- backingMap.put(key, value);
- return Optional.ofNullable(currentValue);
- } finally {
- writeLock.unlock();
- }
+ return delegate.put(key, value);
}
- /**
- * Either return a cached value or compute it and cache it.
- * In case this method is called in parallel for the same key, the value actually present in the map may be from
- * any of the calls.
- * This method always returns either the existing value from the map, or the value provided by the supplier. It
- * never returns a result from another thread's supplier.
- *
- * @param key key to check/insert value for
- * @param valueSupplier supplier called if the value is not yet cached, or is invalid
- * @return current value from the cache, or computed value from the supplier
- */
+ @Override
public Optional computeValue(K key, Supplier> valueSupplier) {
- // get is properly synchronized
- Optional currentValue = get(key);
- if (currentValue.isPresent()) {
- return currentValue;
- }
- Optional newValue = valueSupplier.get();
- // put is also properly synchronized - nevertheless we may replace the value more then once
- // if called from parallel threads
- newValue.ifPresent(theValue -> put(key, theValue));
-
- return newValue;
+ return delegate.computeValue(key, valueSupplier);
}
- /**
- * Current size of the map.
- *
- * @return number of records currently cached
- */
+ @Override
public int size() {
- readLock.lock();
- try {
- return backingMap.size();
- } finally {
- readLock.unlock();
- }
+ return delegate.size();
}
- /**
- * Capacity of this cache.
- *
- * @return configured capacity of this cache
- */
+ @Override
public int capacity() {
- return capacity;
+ return delegate.capacity();
}
- /**
- * Clear all records in the cache.
- */
+ @Override
public void clear() {
- writeLock.lock();
- try {
- backingMap.clear();
- } finally {
- writeLock.unlock();
- }
- }
-
- // for unit testing
- V directGet(K key) {
- return backingMap.get(key);
+ delegate.clear();
}
}
diff --git a/common/mapper/pom.xml b/common/mapper/pom.xml
index 567fb46285c..23f2b7e8350 100644
--- a/common/mapper/pom.xml
+++ b/common/mapper/pom.xml
@@ -41,6 +41,18 @@
io.helidon.commonhelidon-common
+
+ io.helidon.builder
+ helidon-builder-api
+
+
+ io.helidon.service
+ helidon-service-registry
+
+
+ io.helidon.common
+ helidon-common-types
+ org.junit.jupiterjunit-jupiter-api
@@ -52,4 +64,59 @@
test
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.helidon.codegen
+ helidon-codegen-apt
+ ${helidon.version}
+
+
+ io.helidon.builder
+ helidon-builder-codegen
+ ${helidon.version}
+
+
+ io.helidon.service
+ helidon-service-codegen
+ ${helidon.version}
+
+
+ io.helidon.codegen
+ helidon-codegen-helidon-copyright
+ ${helidon.version}
+
+
+
+
+
+ io.helidon.codegen
+ helidon-codegen-apt
+ ${helidon.version}
+
+
+ io.helidon.builder
+ helidon-builder-codegen
+ ${helidon.version}
+
+
+ io.helidon.service
+ helidon-service-codegen
+ ${helidon.version}
+
+
+ io.helidon.codegen
+ helidon-codegen-helidon-copyright
+ ${helidon.version}
+
+
+
+
+
diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/BuiltInMappers.java b/common/mapper/src/main/java/io/helidon/common/mapper/BuiltInMappers.java
index e2a42d4333d..f54cac3f981 100644
--- a/common/mapper/src/main/java/io/helidon/common/mapper/BuiltInMappers.java
+++ b/common/mapper/src/main/java/io/helidon/common/mapper/BuiltInMappers.java
@@ -44,6 +44,7 @@
import java.util.function.Function;
import java.util.regex.Pattern;
+import io.helidon.common.Weight;
import io.helidon.common.mapper.spi.MapperProvider;
/*
@@ -51,6 +52,7 @@
* This does not contain many of date/time mapping, as that is context sensitive (e.g. you need a different
* format depending on the component used).
*/
+@Weight(10)
class BuiltInMappers implements MapperProvider {
private static final Map> MAPPERS;
diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/DefaultMapperProvider.java b/common/mapper/src/main/java/io/helidon/common/mapper/DefaultMapperProvider.java
index 88bdef3ab83..e776cfbb9e7 100644
--- a/common/mapper/src/main/java/io/helidon/common/mapper/DefaultMapperProvider.java
+++ b/common/mapper/src/main/java/io/helidon/common/mapper/DefaultMapperProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 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.
@@ -19,8 +19,12 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import io.helidon.common.Weight;
import io.helidon.common.mapper.spi.MapperProvider;
+import io.helidon.service.registry.Service;
+@Service.Singleton
+@Weight(0.1)
class DefaultMapperProvider implements MapperProvider {
private static final Map CACHE = new ConcurrentHashMap<>();
@@ -38,7 +42,6 @@ public ProviderResponse mapper(Class> sourceClass, Class> targetClass, Strin
}
private static ProviderResponse fromString(Class> target) {
- // todo add all supported types
if (target.equals(int.class) || target.equals(Integer.class)) {
return new ProviderResponse(Support.COMPATIBLE, o -> Integer.parseInt((String) o));
}
@@ -49,7 +52,6 @@ private static ProviderResponse fromString(Class> target) {
}
private static ProviderResponse toString(Class> source) {
- // todo add all supported types (explicitly)
return new ProviderResponse(Support.COMPATIBLE, String::valueOf);
}
diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/GlobalManager.java b/common/mapper/src/main/java/io/helidon/common/mapper/GlobalManager.java
index 2aeae42d9e2..06e5fda532d 100644
--- a/common/mapper/src/main/java/io/helidon/common/mapper/GlobalManager.java
+++ b/common/mapper/src/main/java/io/helidon/common/mapper/GlobalManager.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.
@@ -16,25 +16,42 @@
package io.helidon.common.mapper;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
-import io.helidon.common.LazyValue;
+import io.helidon.service.registry.Services;
+// this class should be removed when deprecation is resolved
+@SuppressWarnings("removal")
final class GlobalManager {
- private static final LazyValue DEFAULT_MAPPER = LazyValue.create(() -> MapperManager.builder()
- .useBuiltIn(true)
- .build());
+ private static final System.Logger LOGGER = System.getLogger(GlobalManager.class.getName());
+ private static final AtomicBoolean LOGGED_REGISTERED = new AtomicBoolean(false);
+
private static final AtomicReference MANAGER = new AtomicReference<>();
private GlobalManager() {
}
- public static void mapperManager(MapperManager manager) {
+ static void mapperManager(MapperManager manager) {
MANAGER.set(manager);
+
+ try {
+ Services.set(Mappers.class, manager);
+ } catch (Exception e) {
+ if (LOGGED_REGISTERED.compareAndSet(false, true)) {
+ // only log this once
+ LOGGER.log(System.Logger.Level.WARNING,
+ "Attempting to set a Mappers (MapperManager) instance when it either was already "
+ + "set once, or it was already used by a component."
+ + " This will not work in future versions of"
+ + " Helidon",
+ e);
+ }
+ }
}
static MapperManager mapperManager() {
MapperManager mapperManager = MANAGER.get();
- return mapperManager == null ? DEFAULT_MAPPER.get() : mapperManager;
+ return mapperManager == null ? Services.get(MapperManager.class) : mapperManager;
}
}
diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/Mapper.java b/common/mapper/src/main/java/io/helidon/common/mapper/Mapper.java
index 2c2786c4d78..3208e9c6cb1 100644
--- a/common/mapper/src/main/java/io/helidon/common/mapper/Mapper.java
+++ b/common/mapper/src/main/java/io/helidon/common/mapper/Mapper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2021 Oracle and/or its affiliates.
+ * Copyright (c) 2020, 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.
@@ -15,8 +15,11 @@
*/
package io.helidon.common.mapper;
+import java.util.Set;
import java.util.function.Function;
+import io.helidon.common.GenericType;
+
/**
* A generic and general approach to mapping two types.
* A mapper is unidirectional - from {@code SOURCE} to {@code TARGET}.
@@ -38,4 +41,41 @@ public interface Mapper extends Function {
default TARGET apply(SOURCE source){
return map(source);
}
+
+ /**
+ * Source type of the mapper. This does not need to be implemented when registered using
+ * {@link io.helidon.common.mapper.MappersConfig.Builder} {@code addMapper} methods.
+ *
+ * It MUST be implemented when implementing a service (i.e. {@link io.helidon.service.registry.Service.Singleton}).
+ *
+ * @return type of the source this mapper support
+ */
+ default GenericType sourceType() {
+ throw new IllegalStateException("If you create a mapper service, you need to implement the sourceType() and targetType() "
+ + "methods");
+ }
+
+ /**
+ * Target type of the mapper. This does not need to be implemented when registered using
+ * {@link io.helidon.common.mapper.MappersConfig.Builder} {@code addMapper} methods.
+ *
+ * It MUST be implemented when implementing a service (i.e. {@link io.helidon.service.registry.Service.Singleton}).
+ *
+ * @return type of the target this mapper support
+ */
+ default GenericType targetType() {
+ throw new IllegalStateException("If you create a mapper service, you need to implement the sourceType() and targetType() "
+ + "methods");
+ }
+
+ /**
+ * Qualifiers of the mapper. This is only used when the mapper is provided as
+ * {@link io.helidon.service.registry.ServiceRegistry} service, otherwise qualifiers provided when registering this
+ * mapper are used.
+ *
+ * @return qualifiers of this mapper, defaults to empty set
+ */
+ default Set qualifiers() {
+ return Set.of();
+ }
}
diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/MapperManager.java b/common/mapper/src/main/java/io/helidon/common/mapper/MapperManager.java
index bb07fabb0fb..da33c63261f 100644
--- a/common/mapper/src/main/java/io/helidon/common/mapper/MapperManager.java
+++ b/common/mapper/src/main/java/io/helidon/common/mapper/MapperManager.java
@@ -38,14 +38,20 @@
* 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)
*
+ *
+ * @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