diff --git a/service/registry/src/main/java/io/helidon/service/registry/LookupBlueprint.java b/service/registry/src/main/java/io/helidon/service/registry/LookupBlueprint.java index 6526ed1eb00..dec0bd03da1 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/LookupBlueprint.java +++ b/service/registry/src/main/java/io/helidon/service/registry/LookupBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -172,13 +172,21 @@ default boolean matches(ServiceInfo serviceInfo) { || this.contracts().contains(ResolvedType.create(serviceInfo.serviceType())) || serviceInfo.factoryContracts().containsAll(this.contracts()); } - return matches + matches = matches && matchesProviderTypes(factoryTypes(), serviceInfo.factoryType()) && matchesAbstract(includeAbstract(), serviceInfo.isAbstract()) && (this.scopes().isEmpty() || this.scopes().contains(serviceInfo.scope())) - && Qualifiers.matchesQualifiers(serviceInfo.qualifiers(), this.qualifiers()) && matchesWeight(serviceInfo, this) && matchesOptionals(serviceInfo.runLevel(), this.runLevel()); + + if (serviceInfo.factoryType() == FactoryType.SERVICES) { + // if the service info is a services factory, it may have qualifiers defined at runtime, + // resolve based on instances (i.e. later) + return matches; + } + + return matches + && Qualifiers.matchesQualifiers(serviceInfo.qualifiers(), this.qualifiers()); } /** diff --git a/service/registry/src/main/java/io/helidon/service/registry/LookupSupport.java b/service/registry/src/main/java/io/helidon/service/registry/LookupSupport.java index d8892a94845..871684148b6 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/LookupSupport.java +++ b/service/registry/src/main/java/io/helidon/service/registry/LookupSupport.java @@ -112,6 +112,29 @@ static void serviceType(Lookup.BuilderBase builder, Class contract) { builder.serviceType(TypeName.create(contract)); } + /** + * Only lookup services with the provided named qualifier. + * + * @param builder builder instance + * @param name the name qualifier (use {@link io.helidon.service.registry.Service.Named#WILDCARD_NAME} to find all + */ + @Prototype.BuilderMethod + static void named(Lookup.BuilderBase builder, String name) { + builder.addQualifier(Qualifier.createNamed(name)); + } + + /** + * Only lookup services with the provided named qualifier, where name is the fully qualified name of the class. + * + * @param builder builder instance + * @param clazz fully qualified name of the class is the name qualifier to use + * @see #named(String) + */ + @Prototype.BuilderMethod + static void named(Lookup.BuilderBase builder, Class clazz) { + builder.addQualifier(Qualifier.createNamed(clazz)); + } + private static Lookup createEmpty() { return Lookup.builder().build(); } @@ -133,7 +156,8 @@ public void decorate(Lookup.BuilderBase builder, Optional depe // clear if contained only IP stuff boolean shouldClear = builder.qualifiers().equals(existing.qualifiers()); - if (!(builder.contracts().contains(ResolvedType.create(existing.contract())) + if (!( + builder.contracts().contains(ResolvedType.create(existing.contract())) && builder.contracts().size() == 1)) { shouldClear = false; } diff --git a/service/tests/inject/src/main/java/io/helidon/service/tests/inject/NamedServicesFactoryTypes.java b/service/tests/inject/src/main/java/io/helidon/service/tests/inject/NamedServicesFactoryTypes.java new file mode 100644 index 00000000000..879b3aac8f9 --- /dev/null +++ b/service/tests/inject/src/main/java/io/helidon/service/tests/inject/NamedServicesFactoryTypes.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2025 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.service.tests.inject; + +import java.util.List; +import java.util.stream.Collectors; + +import io.helidon.service.registry.Qualifier; +import io.helidon.service.registry.Service; +import io.helidon.service.registry.ServiceInstance; + +final class NamedServicesFactoryTypes { + private NamedServicesFactoryTypes() { + } + + interface NamedConfig { + String name(); + } + + interface TargetType { + NamedConfig config(); + } + + @Service.Singleton + static class ConfigFactory implements Service.ServicesFactory { + private final List configs; + + @Service.Inject + ConfigFactory() { + configs = List.of(new NamedConfigImpl("first"), + new NamedConfigImpl("second"), + new NamedConfigImpl("third")); + } + + ConfigFactory(List configs) { + this.configs = configs; + } + + @Override + public List> services() { + return configs.stream() + .map(it -> Service.QualifiedInstance.create(it, Qualifier.createNamed(it.name()))) + .collect(Collectors.toUnmodifiableList()); + } + } + + @Service.Singleton + @Service.PerInstance(NamedConfig.class) + static class TargetTypeProvider implements TargetType { + private final NamedConfig config; + + @Service.Inject + TargetTypeProvider(@Service.InstanceName String name, + NamedConfig config, + List> emptyList) { + this.config = config; + if (!name.equals(config.name())) { + throw new IllegalStateException("Got name: " + name + " but config is named: " + config.name()); + } + } + + @Override + public NamedConfig config() { + return config; + } + } + + @Service.Singleton + static class NamedReceiver { + private final NamedConfig config; + private final List all; + + @Service.Inject + NamedReceiver(@Service.Named("second") NamedConfig config, + @Service.Named(Service.Named.WILDCARD_NAME) List all) { + this.config = config; + this.all = all; + } + + String name() { + return config.name(); + } + + List allNames() { + return all.stream() + .map(NamedConfig::name) + .collect(Collectors.toList()); + } + } + + static class NamedConfigImpl implements NamedConfig { + private final String name; + + NamedConfigImpl(String name) { + this.name = name; + } + + @Override + public String name() { + return name; + } + } +} diff --git a/service/tests/inject/src/main/java/io/helidon/service/tests/inject/ServicesFactoryTypes.java b/service/tests/inject/src/main/java/io/helidon/service/tests/inject/ServicesFactoryTypes.java index a8b31b6b4a6..f8577317265 100644 --- a/service/tests/inject/src/main/java/io/helidon/service/tests/inject/ServicesFactoryTypes.java +++ b/service/tests/inject/src/main/java/io/helidon/service/tests/inject/ServicesFactoryTypes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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,78 +16,90 @@ package io.helidon.service.tests.inject; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.List; -import java.util.stream.Collectors; +import io.helidon.common.types.Annotation; import io.helidon.service.registry.Qualifier; import io.helidon.service.registry.Service; -import io.helidon.service.registry.ServiceInstance; final class ServicesFactoryTypes { private ServicesFactoryTypes() { } - interface NamedConfig { - String name(); + @Target({ElementType.TYPE, ElementType.PARAMETER}) + @Retention(RetentionPolicy.CLASS) + @Service.Qualifier + @interface FirstQualifier { + Qualifier QUALIFIER = Qualifier.create(Annotation.create(FirstQualifier.class)); } - interface TargetType { - NamedConfig config(); + @Target({ElementType.TYPE, ElementType.PARAMETER}) + @Retention(RetentionPolicy.CLASS) + @Service.Qualifier + @interface SecondQualifier { + Qualifier QUALIFIER = Qualifier.create(Annotation.create(SecondQualifier.class)); } - @Service.Singleton - static class ConfigFactory implements Service.ServicesFactory { - private final List configs; - - @Service.Inject - ConfigFactory() { - configs = List.of(new NamedConfigImpl("first"), - new NamedConfigImpl("second"), - new NamedConfigImpl("third")); - } + interface QualifiedContract { + String description(); + } - ConfigFactory(List configs) { - this.configs = configs; - } + @Service.Singleton + static class ContractFactory implements Service.ServicesFactory { @Override - public List> services() { - return configs.stream() - .map(it -> Service.QualifiedInstance.create(it, Qualifier.createNamed(it.name()))) - .collect(Collectors.toUnmodifiableList()); + public List> services() { + return List.of( + Service.QualifiedInstance.create(new QualifiedImpl("first"), FirstQualifier.QUALIFIER), + Service.QualifiedInstance.create(new QualifiedImpl("second"), SecondQualifier.QUALIFIER), + Service.QualifiedInstance.create(new QualifiedImpl("both"), + FirstQualifier.QUALIFIER, + SecondQualifier.QUALIFIER) + ); } } @Service.Singleton - @Service.PerInstance(NamedConfig.class) - static class TargetTypeProvider implements TargetType { - private final NamedConfig config; + static class QualifiedReceiver { + private final QualifiedContract first; + private final QualifiedContract second; + private final QualifiedContract third; @Service.Inject - TargetTypeProvider(@Service.InstanceName String name, - NamedConfig config, - List> emptyList) { - this.config = config; - if (!name.equals(config.name())) { - throw new IllegalStateException("Got name: " + name + " but config is named: " + config.name()); - } + QualifiedReceiver(@FirstQualifier QualifiedContract first, + @SecondQualifier QualifiedContract second, + @FirstQualifier @SecondQualifier QualifiedContract third) { + this.first = first; + this.second = second; + this.third = third; } - @Override - public NamedConfig config() { - return config; + QualifiedContract first() { + return first; + } + + QualifiedContract second() { + return second; + } + + QualifiedContract third() { + return third; } } - static class NamedConfigImpl implements NamedConfig { + static class QualifiedImpl implements QualifiedContract { private final String name; - NamedConfigImpl(String name) { + QualifiedImpl(String name) { this.name = name; } @Override - public String name() { + public String description() { return name; } } diff --git a/service/tests/inject/src/test/java/io/helidon/service/tests/inject/NamedServicesFactoryTest.java b/service/tests/inject/src/test/java/io/helidon/service/tests/inject/NamedServicesFactoryTest.java new file mode 100644 index 00000000000..d2cb37d5031 --- /dev/null +++ b/service/tests/inject/src/test/java/io/helidon/service/tests/inject/NamedServicesFactoryTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2025 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.service.tests.inject; + +import java.util.List; +import java.util.stream.Collectors; + +import io.helidon.service.registry.Lookup; +import io.helidon.service.registry.Qualifier; +import io.helidon.service.registry.ServiceRegistry; +import io.helidon.service.registry.ServiceRegistryConfig; +import io.helidon.service.registry.ServiceRegistryManager; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; + +public class NamedServicesFactoryTest { + private static ServiceRegistryManager registryManager; + private static ServiceRegistry registry; + + @BeforeAll + public static void initRegistry() { + var injectConfig = ServiceRegistryConfig.builder() + .addServiceDescriptor(NamedServicesFactoryTypes_TargetTypeProvider__ServiceDescriptor.INSTANCE) + .addServiceDescriptor(NamedServicesFactoryTypes_ConfigFactory__ServiceDescriptor.INSTANCE) + .addServiceDescriptor(NamedServicesFactoryTypes_NamedReceiver__ServiceDescriptor.INSTANCE) + .discoverServices(false) + .discoverServicesFromServiceLoader(false) + .build(); + registryManager = ServiceRegistryManager.create(injectConfig); + registry = registryManager.registry(); + } + + @AfterAll + public static void tearDownRegistry() { + if (registryManager != null) { + registryManager.shutdown(); + } + } + + @Test + void testInjected() { + var injected = registry.get(NamedServicesFactoryTypes.NamedReceiver.class); + + assertThat(injected.name(), is("second")); + assertThat(injected.allNames(), hasItems("first", "second", "third")); + } + + @Test + void testServicesFactoryConfig() { + List targetTypes = + registry.all(Lookup.builder() + .addQualifier(Qualifier.WILDCARD_NAMED) + .addContract(NamedServicesFactoryTypes.NamedConfig.class) + .build()); + + assertThat(targetTypes, hasSize(3)); + var names = targetTypes.stream() + .map(NamedServicesFactoryTypes.NamedConfig::name) + .collect(Collectors.toUnmodifiableSet()); + + assertThat(names, hasItems("first", "second", "third")); + } + + @Test + void testServicesFactoryConfigFirst() { + NamedServicesFactoryTypes.NamedConfig instance = + registry.get(Lookup.builder() + .named("first") + .addContract(NamedServicesFactoryTypes.NamedConfig.class) + .build()); + + assertThat(instance.name(), is("first")); + } + + @Test + void testServicesFactoryConfigSecond() { + NamedServicesFactoryTypes.NamedConfig instance = + registry.get(Lookup.builder() + .named("second") + .addContract(NamedServicesFactoryTypes.NamedConfig.class) + .build()); + + assertThat(instance.name(), is("second")); + } + + @Test + void testServicesFactoryConfigThird() { + NamedServicesFactoryTypes.NamedConfig instance = + registry.get(Lookup.builder() + .named("third") + .addContract(NamedServicesFactoryTypes.NamedConfig.class) + .build()); + + assertThat(instance.name(), is("third")); + } + + @Test + void testServicesFactory() { + List targetTypes = + registry.all(Lookup.builder() + .addQualifier(Qualifier.WILDCARD_NAMED) + .addContract(NamedServicesFactoryTypes.TargetType.class) + .build()); + + assertThat(targetTypes, hasSize(3)); + var names = targetTypes.stream() + .map(NamedServicesFactoryTypes.TargetType::config) + .map(NamedServicesFactoryTypes.NamedConfig::name) + .collect(Collectors.toUnmodifiableSet()); + + assertThat(names, hasItems("first", "second", "third")); + } + + @Test + void testServicesFactoryFirst() { + NamedServicesFactoryTypes.TargetType instance = + registry.get(Lookup.builder() + .named("first") + .addContract(NamedServicesFactoryTypes.TargetType.class) + .build()); + + assertThat(instance.config().name(), is("first")); + } + + @Test + void testServicesFactorySecond() { + NamedServicesFactoryTypes.TargetType instance = + registry.get(Lookup.builder() + .named("second") + .addContract(NamedServicesFactoryTypes.TargetType.class) + .build()); + + assertThat(instance.config().name(), is("second")); + } + + @Test + void testServicesFactoryThird() { + NamedServicesFactoryTypes.TargetType instance = + registry.get(Lookup.builder() + .named("third") + .addContract(NamedServicesFactoryTypes.TargetType.class) + .build()); + + assertThat(instance.config().name(), is("third")); + } +} diff --git a/service/tests/inject/src/test/java/io/helidon/service/tests/inject/ServicesFactoryProvidedInstancesTest.java b/service/tests/inject/src/test/java/io/helidon/service/tests/inject/ServicesFactoryProvidedInstancesTest.java index 63ed0744ba0..c4d376a51da 100644 --- a/service/tests/inject/src/test/java/io/helidon/service/tests/inject/ServicesFactoryProvidedInstancesTest.java +++ b/service/tests/inject/src/test/java/io/helidon/service/tests/inject/ServicesFactoryProvidedInstancesTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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 @@ public class ServicesFactoryProvidedInstancesTest { @BeforeAll public static void initRegistry() { var injectConfig = ServiceRegistryConfig.builder() - .addServiceDescriptor(ServicesFactoryTypes_TargetTypeProvider__ServiceDescriptor.INSTANCE) - .addServiceDescriptor(ServicesFactoryTypes_ConfigFactory__ServiceDescriptor.INSTANCE) - .putServiceInstance(ServicesFactoryTypes_ConfigFactory__ServiceDescriptor.INSTANCE, - new ServicesFactoryTypes.ConfigFactory(List.of(new ServicesFactoryTypes.NamedConfigImpl( + .addServiceDescriptor(NamedServicesFactoryTypes_TargetTypeProvider__ServiceDescriptor.INSTANCE) + .addServiceDescriptor(NamedServicesFactoryTypes_ConfigFactory__ServiceDescriptor.INSTANCE) + .putServiceInstance(NamedServicesFactoryTypes_ConfigFactory__ServiceDescriptor.INSTANCE, + new NamedServicesFactoryTypes.ConfigFactory(List.of(new NamedServicesFactoryTypes.NamedConfigImpl( "custom")))) .discoverServices(false) .discoverServicesFromServiceLoader(false) @@ -61,16 +61,16 @@ public static void tearDownRegistry() { @Test void testServicesFactory() { - List targetTypes = + List targetTypes = registry.all(Lookup.builder() .addQualifier(Qualifier.WILDCARD_NAMED) - .addContract(ServicesFactoryTypes.TargetType.class) + .addContract(NamedServicesFactoryTypes.TargetType.class) .build()); assertThat(targetTypes, hasSize(1)); var names = targetTypes.stream() - .map(ServicesFactoryTypes.TargetType::config) - .map(ServicesFactoryTypes.NamedConfig::name) + .map(NamedServicesFactoryTypes.TargetType::config) + .map(NamedServicesFactoryTypes.NamedConfig::name) .collect(Collectors.toUnmodifiableSet()); assertThat(names, hasItems("custom")); diff --git a/service/tests/inject/src/test/java/io/helidon/service/tests/inject/ServicesFactoryTest.java b/service/tests/inject/src/test/java/io/helidon/service/tests/inject/ServicesFactoryTest.java index c996e28ba3f..9b194cbd69f 100644 --- a/service/tests/inject/src/test/java/io/helidon/service/tests/inject/ServicesFactoryTest.java +++ b/service/tests/inject/src/test/java/io/helidon/service/tests/inject/ServicesFactoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; @@ -40,8 +41,8 @@ public class ServicesFactoryTest { @BeforeAll public static void initRegistry() { var injectConfig = ServiceRegistryConfig.builder() - .addServiceDescriptor(ServicesFactoryTypes_TargetTypeProvider__ServiceDescriptor.INSTANCE) - .addServiceDescriptor(ServicesFactoryTypes_ConfigFactory__ServiceDescriptor.INSTANCE) + .addServiceDescriptor(ServicesFactoryTypes_ContractFactory__ServiceDescriptor.INSTANCE) + .addServiceDescriptor(ServicesFactoryTypes_QualifiedReceiver__ServiceDescriptor.INSTANCE) .discoverServices(false) .discoverServicesFromServiceLoader(false) .build(); @@ -56,20 +57,62 @@ public static void tearDownRegistry() { } } + @Test + void testInjected() { + var injected = registry.get(ServicesFactoryTypes.QualifiedReceiver.class); + + assertThat(injected.first().description(), is("first")); + assertThat(injected.second().description(), is("second")); + assertThat(injected.third().description(), is("both")); + } + @Test void testServicesFactory() { - List targetTypes = + List targetTypes = registry.all(Lookup.builder() .addQualifier(Qualifier.WILDCARD_NAMED) - .addContract(ServicesFactoryTypes.TargetType.class) + .addContract(ServicesFactoryTypes.QualifiedContract.class) .build()); assertThat(targetTypes, hasSize(3)); var names = targetTypes.stream() - .map(ServicesFactoryTypes.TargetType::config) - .map(ServicesFactoryTypes.NamedConfig::name) + .map(ServicesFactoryTypes.QualifiedContract::description) .collect(Collectors.toUnmodifiableSet()); - assertThat(names, hasItems("first", "second", "third")); + assertThat(names, hasItems("first", "second", "both")); + } + + @Test + void testServicesFactoryFirst() { + ServicesFactoryTypes.QualifiedContract instance = + registry.get(Lookup.builder() + .addQualifier(ServicesFactoryTypes.FirstQualifier.QUALIFIER) + .addContract(ServicesFactoryTypes.QualifiedContract.class) + .build()); + + assertThat(instance.description(), is("first")); + } + + @Test + void testServicesFactorySecond() { + ServicesFactoryTypes.QualifiedContract instance = + registry.get(Lookup.builder() + .addQualifier(ServicesFactoryTypes.SecondQualifier.QUALIFIER) + .addContract(ServicesFactoryTypes.QualifiedContract.class) + .build()); + + assertThat(instance.description(), is("second")); + } + + @Test + void testServicesFactoryThird() { + ServicesFactoryTypes.QualifiedContract instance = + registry.get(Lookup.builder() + .addQualifier(ServicesFactoryTypes.FirstQualifier.QUALIFIER) + .addQualifier(ServicesFactoryTypes.SecondQualifier.QUALIFIER) + .addContract(ServicesFactoryTypes.QualifiedContract.class) + .build()); + + assertThat(instance.description(), is("both")); } }