Skip to content

Commit

Permalink
Provider support in builders (#7365)
Browse files Browse the repository at this point in the history
* Support for services from service loader that have a single or optional instance.
* Support Config config(), and Optional<Config> config() methods in blueprints
  • Loading branch information
tomas-langer committed Aug 14, 2023
1 parent 5519bb0 commit e253a3f
Show file tree
Hide file tree
Showing 19 changed files with 761 additions and 29 deletions.
28 changes: 27 additions & 1 deletion builder/api/src/main/java/io/helidon/builder/api/Prototype.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;
Expand Down Expand Up @@ -102,7 +103,7 @@ public interface ConfiguredBuilder<BUILDER, PROTOTYPE> extends Builder<BUILDER,
* node
* @param <S> type of the expected service
* @param <T> type of the configured service provider that creates instances of S
* @return list of discovered services
* @return list of discovered services, ordered by {@link io.helidon.common.Weight} (highest weight is first in the list)
*/
default <S extends NamedService, T extends ConfiguredProvider<S>> List<S>
discoverServices(Config config,
Expand All @@ -112,6 +113,31 @@ public interface ConfiguredBuilder<BUILDER, PROTOTYPE> extends Builder<BUILDER,
boolean allFromServiceLoader) {
return ProvidedUtil.discoverServices(config, serviceLoader, providerType, configType, allFromServiceLoader);
}

/**
* Discover service from configuration.
*
* @param config configuration located at the node of the service providers
* (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 <S> type of the expected service
* @param <T> 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
*/
default <S extends NamedService, T extends ConfiguredProvider<S>> Optional<S>
discoverService(Config config,
HelidonServiceLoader<T> serviceLoader,
Class<T> providerType,
Class<S> configType,
boolean allFromServiceLoader) {
return ProvidedUtil.discoverService(config, serviceLoader, providerType, configType, allFromServiceLoader);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import io.helidon.common.HelidonServiceLoader;
Expand Down Expand Up @@ -58,6 +59,60 @@ final class ProvidedUtil {
private ProvidedUtil() {
}

/**
* Discover service from configuration.
* This method looks for a single provider only.
*
* @param config configuration located at the node of the service providers
* (either a list node, or object, where each child is one service)
* @param serviceLoader helidon service loader for the expected type
* @param providerType service provider interface type
* @param configType configured service type
* @param discoverServices whether all services from service loader should be used, or only the ones with configured
* node
* @param <T> type of the expected service
* @return first service by {@link io.helidon.common.Weight}, or empty optional
*/
static <T extends NamedService> Optional<T>
discoverService(Config config,
HelidonServiceLoader<? extends ConfiguredProvider<T>> serviceLoader,
Class<? extends ConfiguredProvider<T>> providerType,
Class<T> configType,
boolean discoverServices) {
/*
- if we find more than one using service loader, we will use one with higher weight, unless a provider is configured
in config
- if we find more than one in config, it is an error
*/
List<ConfiguredService> configuredServices = new ArrayList<>();

// all child nodes of the current node
List<Config> serviceConfigList = config.asNodeList()
.orElseGet(List::of);

if (serviceConfigList.size() > 1) {
throw new ConfigException("There can only be one provider configured for " + config.key());
}

boolean isList = config.isList();

for (Config serviceConfig : serviceConfigList) {
configuredServices.add(configuredService(serviceConfig, isList));
}

List<T> result;
// now we have all service configurations, we can start building up instances
if (config.isList()) {
// driven by order of declaration in config
result = servicesFromList(serviceLoader, providerType, configType, configuredServices, discoverServices);
} else {
// driven by service loader order
result = servicesFromObject(serviceLoader, providerType, configType, configuredServices, discoverServices);
}

return result.isEmpty() ? Optional.empty() : Optional.of(result.get(0));
}

/**
* Discover services from configuration.
*
Expand All @@ -69,7 +124,7 @@ private ProvidedUtil() {
* @param allFromServiceLoader whether all services from service loader should be used, or only the ones with configured
* node
* @param <T> type of the expected service
* @return list of discovered services
* @return list of discovered services, ordered by {@link io.helidon.common.Weight} (highest weight first)
*/
static <T extends NamedService> List<T> discoverServices(Config config,
HelidonServiceLoader<? extends ConfiguredProvider<T>> serviceLoader,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,7 @@ public BUILDER config(Config config) {
pw.println("@Override");
pw.print(SOURCE_SPACING);
pw.print(SOURCE_SPACING);
pw.print("public BUILDER");
pw.println(" config(Config config) {");
pw.println("public BUILDER config(Config config) {");
pw.print(SOURCE_SPACING);
pw.print(SOURCE_SPACING);
pw.print(SOURCE_SPACING);
Expand All @@ -337,7 +336,7 @@ public BUILDER config(Config config) {
if (!child.configuredOption().notConfigured()) {
Optional<String> fromConfig = child.typeHandler().generateFromConfig(child.configuredOption(),
child.factoryMethods());
if (fromConfig.isPresent()) {
if (!child.configuredOption().provider() && fromConfig.isPresent()) {
pw.print(SOURCE_SPACING);
pw.print(SOURCE_SPACING);
pw.print(SOURCE_SPACING);
Expand All @@ -358,6 +357,10 @@ public BUILDER config(Config config) {
TypeName returnType = TypeName.createFromGenericDeclaration("BUILDER");
// first setters
for (PrototypeProperty child : properties) {
if (child.typeHandler().actualType().equals(CONFIG_TYPE)) {
// this is never done here, config must be defined as a standalone method
continue;
}
for (GeneratedMethod setter : child.setters(returnType, child.configuredOption().description())) {
// this is builder setters
Javadoc javadoc = setter.javadoc();
Expand Down Expand Up @@ -416,6 +419,15 @@ public BUILDER config(Config config) {
*/
for (PrototypeProperty child : properties) {
String getterName = child.getterName();
if ("config".equals(getterName) && configured.configured()) {
if (child.typeHandler().actualType().equals(CONFIG_TYPE)) {
// this will always exist
continue;
}
// now we have a method called config with wrong return type - this is not supported
throw new IllegalArgumentException("Configured property named \"config\" can only be of type "
+ CONFIG_TYPE.fqName() + ", but is: " + child.typeName().fqName());
}
/*
String host() {
return host;
Expand Down Expand Up @@ -532,10 +544,21 @@ private static void fromInstanceMethod(PrintWriter pw, TypeContext typeContext,
pw.print(property.typeHandler().getterName());
pw.println("());");
} else {
pw.print(property.typeHandler().setterName());
pw.print("(prototype.");
pw.print(property.typeHandler().getterName());
pw.println("());");
/*
Special handling from config - we have to assign it to field, we cannot go through (config(Config))
*/
if ("config".equals(property.name()) && property.typeHandler().actualType().equals(CONFIG_TYPE)) {
pw.print("this.config = prototype.config()");
if (declaredType.isOptional()) {
pw.print(".orElse(null)");
}
pw.println(";");
} else {
pw.print(property.typeHandler().setterName());
pw.print("(prototype.");
pw.print(property.typeHandler().getterName());
pw.println("());");
}
}
}
pw.print(SOURCE_SPACING);
Expand Down Expand Up @@ -706,25 +729,36 @@ private static void preBuildPrototypeMethod(PrintWriter pw,
pw.print(SOURCE_SPACING);
pw.print(SOURCE_SPACING);
pw.print(SOURCE_SPACING);
pw.print("java.util.List<");
pw.print(property.typeHandler().actualType().fqName());
pw.print("> services = discoverServices(config.get(\"");
pw.print(configuredOption.configKey());
pw.print("\"), serviceLoader, ");
pw.print(providerOption.serviceProviderInterface().fqName());
pw.print(".class, ");
pw.print(property.typeHandler().actualType().fqName());
pw.print(".class, ");
pw.print(property.name());
pw.print("DiscoverServices");
pw.println(");");
pw.print(SOURCE_SPACING);
pw.print(SOURCE_SPACING);
pw.print(SOURCE_SPACING);
pw.print(SOURCE_SPACING);
pw.print("this.add");
pw.print(capitalize(property.name()));
pw.println("(services);");

TypeName typeName = property.typeHandler().declaredType();
if (typeName.isList() || typeName.isSet()) {
pw.print("this.add");
pw.print(capitalize(property.name()));
pw.print("(");
pw.print("discoverServices(config.get(\"");
pw.print(configuredOption.configKey());
pw.print("\"), serviceLoader, ");
pw.print(providerOption.serviceProviderInterface().fqName());
pw.print(".class, ");
pw.print(property.typeHandler().actualType().fqName());
pw.print(".class, ");
pw.print(property.name());
pw.print("DiscoverServices");
pw.println("));");
} else {
pw.print("discoverService(config.get(\"");
pw.print(configuredOption.configKey());
pw.print("\"), serviceLoader, ");
pw.print(providerOption.serviceProviderInterface().fqName());
pw.print(".class, ");
pw.print(property.typeHandler().actualType().fqName());
pw.print(".class, ");
pw.print(property.name());
pw.print("DiscoverServices");
pw.print(").ifPresent(this::");
pw.print(property.setterName());
pw.println(");");
}
} else {
if (defaultDiscoverServices) {
pw.print(SOURCE_SPACING);
Expand Down
14 changes: 14 additions & 0 deletions builder/tests/builder/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@
<artifactId>jackson-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-config</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-yaml</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 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.builder.test.testsubjects;

import io.helidon.builder.api.Prototype;
import io.helidon.common.config.Config;
import io.helidon.config.metadata.Configured;
import io.helidon.config.metadata.ConfiguredOption;

@Prototype.Blueprint
@Configured
interface ConfigMethodBlueprint {
@ConfiguredOption("default-value")
String key();

Config config();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2023 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.builder.test.testsubjects;

import java.util.Optional;

import io.helidon.builder.api.Prototype;
import io.helidon.common.config.Config;
import io.helidon.config.metadata.Configured;
import io.helidon.config.metadata.ConfiguredOption;

@Prototype.Blueprint
@Configured
interface ConfigOptionalMethodBlueprint {
@ConfiguredOption("default-value")
String key();

Optional<Config> config();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2023 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.builder.test.testsubjects;

import io.helidon.common.config.ConfiguredProvider;
import io.helidon.common.config.NamedService;

public interface ProviderNoImpls extends ConfiguredProvider<ProviderNoImpls.SomeService> {
interface SomeService extends NamedService {
String prop();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2023 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.builder.test.testsubjects;

import io.helidon.common.config.ConfiguredProvider;
import io.helidon.common.config.NamedService;

public interface SomeProvider extends ConfiguredProvider<SomeProvider.SomeService> {

interface SomeService extends NamedService {
String prop();
}
}
Loading

0 comments on commit e253a3f

Please sign in to comment.