Skip to content

Commit a8ba0c6

Browse files
authored
Builder - fixed a problem where providers that were not also configured failed to generate valid code. (helidon-io#10296)
1 parent 36ae8c1 commit a8ba0c6

10 files changed

Lines changed: 261 additions & 37 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.builder.api;
18+
19+
import java.util.ArrayList;
20+
import java.util.HashSet;
21+
import java.util.List;
22+
import java.util.Optional;
23+
import java.util.Set;
24+
25+
import io.helidon.common.HelidonServiceLoader;
26+
27+
/**
28+
* Static methods used from generated prototypes and builders.
29+
* <p>
30+
* This only contains methods that can be used without additional dependencies (i.e. {@link java.util.ServiceLoader} based).
31+
*/
32+
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
33+
public final class BuilderSupport {
34+
private BuilderSupport() {
35+
}
36+
37+
/**
38+
* Discover services from Java {@link java.util.ServiceLoader}.
39+
* This method will only add instances of classes that are not already part of {@code existingInstances}.
40+
*
41+
* @param contract service contract (or provider interface as used in {@link java.util.ServiceLoader})
42+
* @param discoverServices whether to discover services from service loader, if set to false, service loader is ignored
43+
* @param existingInstances instances already configured on the builder
44+
* @param <C> type of the contract to use
45+
* @return a list of new instances to add to the builder
46+
*/
47+
public static <C> List<C> discoverServices(Class<C> contract,
48+
boolean discoverServices,
49+
List<C> existingInstances) {
50+
if (!discoverServices) {
51+
return List.of();
52+
}
53+
List<C> newInstances = new ArrayList<>();
54+
Set<Class<?>> existingServiceTypes = new HashSet<>();
55+
existingInstances.forEach(it -> existingServiceTypes.add(it.getClass()));
56+
HelidonServiceLoader.create(contract).forEach(it -> {
57+
if (!existingServiceTypes.contains(it.getClass())) {
58+
newInstances.add(it);
59+
}
60+
});
61+
62+
return newInstances;
63+
}
64+
65+
/**
66+
* Discover a service from {@link java.util.ServiceLoader}.
67+
* This method will only query the service loader if the {@code existingInstance} is empty.
68+
*
69+
* @param contract service contract (or provider interface as used in {@link java.util.ServiceLoader})
70+
* @param discoverServices whether to discover services from service loader, if set to false, service loader is ignored
71+
* @param existingInstance an instance configured on the builder
72+
* @param <C> type of the contract
73+
* @return value to be used by the builder (either the existing instance, an instance from {@link java.util.ServiceLoader},
74+
* or empty if none found
75+
*/
76+
public static <C> Optional<C> discoverService(Class<C> contract,
77+
boolean discoverServices,
78+
Optional<C> existingInstance) {
79+
if (existingInstance.isPresent() || !discoverServices) {
80+
return existingInstance;
81+
}
82+
83+
return HelidonServiceLoader.create(contract)
84+
.stream()
85+
.findFirst();
86+
}
87+
}

builder/api/src/main/java/io/helidon/builder/api/Option.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ private Option() {
146146
* When set to {@code true}, all services discovered by the service loader will be added (even if no configuration
147147
* node exists for them). When set to {@code false}, only services that have a configuration node will be added.
148148
* This can be overridden by {@code discover-services} configuration option under this option's key.
149+
* In case the option is not {@link io.helidon.builder.api.Option.Configured}, this is always considered true,
150+
* as otherwise this annotation would not make any sense (i.e. it would never provide a value).
149151
*
150152
* @return whether to discover services by default for a provider
151153
*/

builder/codegen/src/main/java/io/helidon/builder/codegen/GenerateAbstractBuilder.java

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023, 2024 Oracle and/or its affiliates.
2+
* Copyright (c) 2023, 2025 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,7 +25,6 @@
2525
import java.util.Locale;
2626
import java.util.Objects;
2727
import java.util.Optional;
28-
import java.util.ServiceLoader;
2928
import java.util.Set;
3029
import java.util.function.Supplier;
3130
import java.util.stream.Collectors;
@@ -38,12 +37,12 @@
3837
import io.helidon.codegen.classmodel.Method;
3938
import io.helidon.codegen.classmodel.TypeArgument;
4039
import io.helidon.common.Errors;
41-
import io.helidon.common.HelidonServiceLoader;
4240
import io.helidon.common.types.AccessModifier;
4341
import io.helidon.common.types.Annotations;
4442
import io.helidon.common.types.TypeName;
4543
import io.helidon.common.types.TypeNames;
4644

45+
import static io.helidon.builder.codegen.Types.BUILDER_SUPPORT;
4746
import static io.helidon.builder.codegen.Types.CONFIG_BUILDER_SUPPORT;
4847
import static io.helidon.builder.codegen.Types.REGISTRY_BUILDER_SUPPORT;
4948
import static io.helidon.codegen.CodegenUtil.capitalize;
@@ -624,8 +623,7 @@ private static void preBuildPrototypeMethod(InnerClass.Builder classBuilder,
624623
property,
625624
propertyConfigured,
626625
configuredOption,
627-
providerType,
628-
defaultDiscoverServices);
626+
providerType);
629627
} else {
630628
serviceLoaderPropertyDiscovery(preBuildBuilder,
631629
property,
@@ -655,24 +653,16 @@ private static void serviceLoaderPropertyDiscovery(Method.Builder preBuildBuilde
655653
AnnotationDataOption configuredOption,
656654
TypeName providerType,
657655
boolean defaultDiscoverServices) {
658-
preBuildBuilder.addContentLine("{");
659-
preBuildBuilder.addContent("var serviceLoader = ")
660-
.addContent(HelidonServiceLoader.class)
661-
.addContent(".create(")
662-
.addContent(ServiceLoader.class)
663-
.addContent(".load(")
664-
.addContent(providerType.genericTypeName())
665-
.addContentLine(".class));");
656+
TypeName typeName = property.typeHandler().declaredType();
666657
if (propertyConfigured) {
667-
TypeName typeName = property.typeHandler().declaredType();
668658
if (typeName.isList() || typeName.isSet()) {
669659
preBuildBuilder.addContent("this.add")
670660
.addContent(capitalize(property.name()))
671661
.addContent("(")
672662
.addContent(CONFIG_BUILDER_SUPPORT)
673663
.addContent(".discoverServices(config, \"")
674664
.addContent(configuredOption.configKey())
675-
.addContent("\", serviceLoader, ")
665+
.addContent("\", ")
676666
.addContent(providerType.genericTypeName())
677667
.addContent(".class, ")
678668
.addContent(property.typeHandler().actualType().genericTypeName())
@@ -686,7 +676,7 @@ private static void serviceLoaderPropertyDiscovery(Method.Builder preBuildBuilde
686676
.addContent(CONFIG_BUILDER_SUPPORT)
687677
.addContent(".discoverService(config, \"")
688678
.addContent(configuredOption.configKey())
689-
.addContent("\", serviceLoader, ")
679+
.addContent("\", ")
690680
.addContent(providerType)
691681
.addContent(".class, ")
692682
.addContent(property.typeHandler().actualType().genericTypeName())
@@ -701,11 +691,34 @@ private static void serviceLoaderPropertyDiscovery(Method.Builder preBuildBuilde
701691
.addContentLine(");");
702692
}
703693
} else {
704-
if (defaultDiscoverServices) {
705-
preBuildBuilder.addContentLine("this." + property.name() + "(serviceLoader.asList());");
694+
if (typeName.isList() || typeName.isSet()) {
695+
preBuildBuilder.addContent("this.add")
696+
.addContent(capitalize(property.name()))
697+
.addContent("(")
698+
.addContent(BUILDER_SUPPORT)
699+
.addContent(".discoverServices(")
700+
.addContent(providerType.genericTypeName())
701+
.addContent(".class, ")
702+
.addContent(property.name())
703+
.addContent("DiscoverServices, ")
704+
.addContent(property.name())
705+
.addContentLine("));");
706+
} else {
707+
preBuildBuilder
708+
.addContent(BUILDER_SUPPORT)
709+
.addContent(".discoverService(")
710+
.addContent(providerType)
711+
.addContent(".class, ")
712+
.addContent(property.name())
713+
.addContent("DiscoverServices, ")
714+
.addContent(Optional.class)
715+
.addContent(".ofNullable(")
716+
.addContent(property.name())
717+
.addContent(")).ifPresent(this::")
718+
.addContent(property.setterName())
719+
.addContentLine(");");
706720
}
707721
}
708-
preBuildBuilder.addContentLine("}");
709722
}
710723

711724
private static void serviceRegistryProperty(Method.Builder preBuildBuilder,
@@ -754,10 +767,9 @@ private static void serviceRegistryPropertyDiscovery(Method.Builder preBuildBuil
754767
PrototypeProperty property,
755768
boolean propertyConfigured,
756769
AnnotationDataOption configuredOption,
757-
TypeName providerType,
758-
boolean defaultDiscoverServices) {
770+
TypeName providerType) {
771+
TypeName typeName = property.typeHandler().declaredType();
759772
if (propertyConfigured) {
760-
TypeName typeName = property.typeHandler().declaredType();
761773
if (typeName.isList() || typeName.isSet()) {
762774
preBuildBuilder.addContent("this.add")
763775
.addContent(capitalize(property.name()))
@@ -811,10 +823,42 @@ private static void serviceRegistryPropertyDiscovery(Method.Builder preBuildBuil
811823
.decreaseContentPadding();
812824
}
813825
} else {
814-
if (defaultDiscoverServices) {
815-
preBuildBuilder.addContent("this." + property.name() + "(registry.all(")
816-
.addContent(providerType.genericTypeName())
817-
.addContentLine(".class));");
826+
if (typeName.isList()) {
827+
preBuildBuilder
828+
.addContent("this.add")
829+
.addContent(capitalize(property.name()))
830+
.addContent("(")
831+
.addContent(REGISTRY_BUILDER_SUPPORT)
832+
.addContent(".serviceList(registry, ")
833+
.addContentCreate(property.typeHandler().actualType())
834+
.addContent(", ")
835+
.addContent(property.name())
836+
.addContentLine("DiscoverServices));");
837+
} else if (typeName.isSet()) {
838+
preBuildBuilder
839+
.addContent("this.add")
840+
.addContent(capitalize(property.name()))
841+
.addContent("(")
842+
.addContent(REGISTRY_BUILDER_SUPPORT)
843+
.addContent(".serviceSet(registry, ")
844+
.addContentCreate(property.typeHandler().actualType())
845+
.addContent(", ")
846+
.addContent(property.name())
847+
.addContentLine("DiscoverServices));");
848+
} else {
849+
preBuildBuilder
850+
.addContent(REGISTRY_BUILDER_SUPPORT)
851+
.addContent(".service(registry, ")
852+
.addContentCreate(property.typeHandler().actualType())
853+
.addContent(", ")
854+
.addContent(Optional.class)
855+
.addContent(".ofNullable(")
856+
.addContent(property.name())
857+
.addContent("), ")
858+
.addContent(property.name())
859+
.addContent("DiscoverServices).ifPresent(this::")
860+
.addContent(property.setterName())
861+
.addContentLine(");");
818862
}
819863
}
820864
}

builder/codegen/src/main/java/io/helidon/builder/codegen/Types.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023, 2024 Oracle and/or its affiliates.
2+
* Copyright (c) 2023, 2025 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -85,6 +85,7 @@ final class Types {
8585

8686
static final TypeName CONFIG_CONFIGURED_BUILDER = TypeName.create(
8787
"io.helidon.common.config.ConfigBuilderSupport.ConfiguredBuilder");
88+
static final TypeName BUILDER_SUPPORT = TypeName.create("io.helidon.builder.api.BuilderSupport");
8889
static final TypeName CONFIG_BUILDER_SUPPORT = TypeName.create("io.helidon.common.config.ConfigBuilderSupport");
8990

9091
static final TypeName REGISTRY_BUILDER_SUPPORT = TypeName.create("io.helidon.service.registry.RegistryBuilderSupport");

builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/WithProviderBlueprint.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023 Oracle and/or its affiliates.
2+
* Copyright (c) 2023, 2025 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -56,6 +56,13 @@ interface WithProviderBlueprint {
5656
@Option.Provider(ProviderNoImpls.class)
5757
Optional<ProviderNoImpls.SomeService> optionalNoImplDiscover();
5858

59+
@Option.Access("")
60+
@Option.Provider(ProviderNoImpls.SomeService.class)
61+
List<ProviderNoImpls.SomeService> listNoImplDiscoverNoConfig();
62+
63+
@Option.Provider(ProviderNoImpls.SomeService.class)
64+
Optional<ProviderNoImpls.SomeService> noImplDiscoverNoConfig();
65+
5966
@Option.Configured
6067
@Option.Provider(value = ProviderNoImpls.class, discoverServices = false)
6168
Optional<ProviderNoImpls.SomeService> optionalNoImplNotDiscover();

builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/WithProviderRegistryBlueprint.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Oracle and/or its affiliates.
2+
* Copyright (c) 2024, 2025 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -67,6 +67,13 @@ interface WithProviderRegistryBlueprint {
6767
@Option.Provider(ProviderNoImpls.class)
6868
List<ProviderNoImpls.SomeService> listNoImplDiscover();
6969

70+
@Option.Access("")
71+
@Option.Provider(ProviderNoImpls.SomeService.class)
72+
List<ProviderNoImpls.SomeService> listNoImplDiscoverNoConfig();
73+
74+
@Option.Provider(ProviderNoImpls.SomeService.class)
75+
Optional<ProviderNoImpls.SomeService> noImplDiscoverNoConfig();
76+
7077
@Option.Configured
7178
@Option.Provider(value = ProviderNoImpls.class, discoverServices = false)
7279
List<ProviderNoImpls.SomeService> listNoImplNotDiscover();

builder/tests/codegen/src/test/java/io/helidon/builder/codegen/TypesTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Oracle and/or its affiliates.
2+
* Copyright (c) 2024, 2025 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
2828
import java.util.Map;
2929
import java.util.Set;
3030

31+
import io.helidon.builder.api.BuilderSupport;
3132
import io.helidon.builder.api.Description;
3233
import io.helidon.builder.api.GeneratedBuilder;
3334
import io.helidon.builder.api.Option;
@@ -133,6 +134,7 @@ void testTypes() {
133134
checkField(toCheck, checked, fields, "OPTION_TYPE", Option.Type.class);
134135
checkField(toCheck, checked, fields, "OPTION_DECORATOR", Option.Decorator.class);
135136
checkField(toCheck, checked, fields, "OPTION_REGISTRY_SERVICE", Option.RegistryService.class);
137+
checkField(toCheck, checked, fields, "BUILDER_SUPPORT", BuilderSupport.class);
136138

137139
checkField(toCheck, checked, fields, "SERVICES", Services.class);
138140

0 commit comments

Comments
 (0)