Skip to content

Commit 2022d84

Browse files
committed
ServicesFactory can now provide any qualified instances (not just named)
1 parent 45ae134 commit 2022d84

File tree

7 files changed

+432
-61
lines changed

7 files changed

+432
-61
lines changed

service/registry/src/main/java/io/helidon/service/registry/LookupBlueprint.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022, 2024 Oracle and/or its affiliates.
2+
* Copyright (c) 2022, 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.
@@ -172,13 +172,21 @@ default boolean matches(ServiceInfo serviceInfo) {
172172
|| this.contracts().contains(ResolvedType.create(serviceInfo.serviceType()))
173173
|| serviceInfo.factoryContracts().containsAll(this.contracts());
174174
}
175-
return matches
175+
matches = matches
176176
&& matchesProviderTypes(factoryTypes(), serviceInfo.factoryType())
177177
&& matchesAbstract(includeAbstract(), serviceInfo.isAbstract())
178178
&& (this.scopes().isEmpty() || this.scopes().contains(serviceInfo.scope()))
179-
&& Qualifiers.matchesQualifiers(serviceInfo.qualifiers(), this.qualifiers())
180179
&& matchesWeight(serviceInfo, this)
181180
&& matchesOptionals(serviceInfo.runLevel(), this.runLevel());
181+
182+
if (serviceInfo.factoryType() == FactoryType.SERVICES) {
183+
// if the service info is a services factory, it may have qualifiers defined at runtime,
184+
// resolve based on instances (i.e. later)
185+
return matches;
186+
}
187+
188+
return matches
189+
&& Qualifiers.matchesQualifiers(serviceInfo.qualifiers(), this.qualifiers());
182190
}
183191

184192
/**

service/registry/src/main/java/io/helidon/service/registry/LookupSupport.java

+25-1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,29 @@ static void serviceType(Lookup.BuilderBase<?, ?> builder, Class<?> contract) {
112112
builder.serviceType(TypeName.create(contract));
113113
}
114114

115+
/**
116+
* Only lookup services with the provided named qualifier.
117+
*
118+
* @param builder builder instance
119+
* @param name the name qualifier (use {@link io.helidon.service.registry.Service.Named#WILDCARD_NAME} to find all
120+
*/
121+
@Prototype.BuilderMethod
122+
static void named(Lookup.BuilderBase<?, ?> builder, String name) {
123+
builder.addQualifier(Qualifier.createNamed(name));
124+
}
125+
126+
/**
127+
* Only lookup services with the provided named qualifier, where name is the fully qualified name of the class.
128+
*
129+
* @param builder builder instance
130+
* @param clazz fully qualified name of the class is the name qualifier to use
131+
* @see #named(String)
132+
*/
133+
@Prototype.BuilderMethod
134+
static void named(Lookup.BuilderBase<?, ?> builder, Class<?> clazz) {
135+
builder.addQualifier(Qualifier.createNamed(clazz));
136+
}
137+
115138
private static Lookup createEmpty() {
116139
return Lookup.builder().build();
117140
}
@@ -133,7 +156,8 @@ public void decorate(Lookup.BuilderBase<?, ?> builder, Optional<Dependency> depe
133156
// clear if contained only IP stuff
134157
boolean shouldClear = builder.qualifiers().equals(existing.qualifiers());
135158

136-
if (!(builder.contracts().contains(ResolvedType.create(existing.contract()))
159+
if (!(
160+
builder.contracts().contains(ResolvedType.create(existing.contract()))
137161
&& builder.contracts().size() == 1)) {
138162
shouldClear = false;
139163
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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.service.tests.inject;
18+
19+
import java.util.List;
20+
import java.util.stream.Collectors;
21+
22+
import io.helidon.service.registry.Qualifier;
23+
import io.helidon.service.registry.Service;
24+
import io.helidon.service.registry.ServiceInstance;
25+
26+
final class NamedServicesFactoryTypes {
27+
private NamedServicesFactoryTypes() {
28+
}
29+
30+
interface NamedConfig {
31+
String name();
32+
}
33+
34+
interface TargetType {
35+
NamedConfig config();
36+
}
37+
38+
@Service.Singleton
39+
static class ConfigFactory implements Service.ServicesFactory<NamedConfig> {
40+
private final List<NamedConfig> configs;
41+
42+
@Service.Inject
43+
ConfigFactory() {
44+
configs = List.of(new NamedConfigImpl("first"),
45+
new NamedConfigImpl("second"),
46+
new NamedConfigImpl("third"));
47+
}
48+
49+
ConfigFactory(List<NamedConfig> configs) {
50+
this.configs = configs;
51+
}
52+
53+
@Override
54+
public List<Service.QualifiedInstance<NamedConfig>> services() {
55+
return configs.stream()
56+
.map(it -> Service.QualifiedInstance.create(it, Qualifier.createNamed(it.name())))
57+
.collect(Collectors.toUnmodifiableList());
58+
}
59+
}
60+
61+
@Service.Singleton
62+
@Service.PerInstance(NamedConfig.class)
63+
static class TargetTypeProvider implements TargetType {
64+
private final NamedConfig config;
65+
66+
@Service.Inject
67+
TargetTypeProvider(@Service.InstanceName String name,
68+
NamedConfig config,
69+
List<ServiceInstance<CharSequence>> emptyList) {
70+
this.config = config;
71+
if (!name.equals(config.name())) {
72+
throw new IllegalStateException("Got name: " + name + " but config is named: " + config.name());
73+
}
74+
}
75+
76+
@Override
77+
public NamedConfig config() {
78+
return config;
79+
}
80+
}
81+
82+
@Service.Singleton
83+
static class NamedReceiver {
84+
private final NamedConfig config;
85+
private final List<NamedConfig> all;
86+
87+
@Service.Inject
88+
NamedReceiver(@Service.Named("second") NamedConfig config,
89+
@Service.Named(Service.Named.WILDCARD_NAME) List<NamedConfig> all) {
90+
this.config = config;
91+
this.all = all;
92+
}
93+
94+
String name() {
95+
return config.name();
96+
}
97+
98+
List<String> allNames() {
99+
return all.stream()
100+
.map(NamedConfig::name)
101+
.collect(Collectors.toList());
102+
}
103+
}
104+
105+
static class NamedConfigImpl implements NamedConfig {
106+
private final String name;
107+
108+
NamedConfigImpl(String name) {
109+
this.name = name;
110+
}
111+
112+
@Override
113+
public String name() {
114+
return name;
115+
}
116+
}
117+
}

service/tests/inject/src/main/java/io/helidon/service/tests/inject/ServicesFactoryTypes.java

+52-40
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.
@@ -16,78 +16,90 @@
1616

1717
package io.helidon.service.tests.inject;
1818

19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
1923
import java.util.List;
20-
import java.util.stream.Collectors;
2124

25+
import io.helidon.common.types.Annotation;
2226
import io.helidon.service.registry.Qualifier;
2327
import io.helidon.service.registry.Service;
24-
import io.helidon.service.registry.ServiceInstance;
2528

2629
final class ServicesFactoryTypes {
2730
private ServicesFactoryTypes() {
2831
}
2932

30-
interface NamedConfig {
31-
String name();
33+
@Target({ElementType.TYPE, ElementType.PARAMETER})
34+
@Retention(RetentionPolicy.CLASS)
35+
@Service.Qualifier
36+
@interface FirstQualifier {
37+
Qualifier QUALIFIER = Qualifier.create(Annotation.create(FirstQualifier.class));
3238
}
3339

34-
interface TargetType {
35-
NamedConfig config();
40+
@Target({ElementType.TYPE, ElementType.PARAMETER})
41+
@Retention(RetentionPolicy.CLASS)
42+
@Service.Qualifier
43+
@interface SecondQualifier {
44+
Qualifier QUALIFIER = Qualifier.create(Annotation.create(SecondQualifier.class));
3645
}
3746

38-
@Service.Singleton
39-
static class ConfigFactory implements Service.ServicesFactory<NamedConfig> {
40-
private final List<NamedConfig> configs;
41-
42-
@Service.Inject
43-
ConfigFactory() {
44-
configs = List.of(new NamedConfigImpl("first"),
45-
new NamedConfigImpl("second"),
46-
new NamedConfigImpl("third"));
47-
}
47+
interface QualifiedContract {
48+
String description();
49+
}
4850

49-
ConfigFactory(List<NamedConfig> configs) {
50-
this.configs = configs;
51-
}
51+
@Service.Singleton
52+
static class ContractFactory implements Service.ServicesFactory<QualifiedContract> {
5253

5354
@Override
54-
public List<Service.QualifiedInstance<NamedConfig>> services() {
55-
return configs.stream()
56-
.map(it -> Service.QualifiedInstance.create(it, Qualifier.createNamed(it.name())))
57-
.collect(Collectors.toUnmodifiableList());
55+
public List<Service.QualifiedInstance<QualifiedContract>> services() {
56+
return List.of(
57+
Service.QualifiedInstance.create(new QualifiedImpl("first"), FirstQualifier.QUALIFIER),
58+
Service.QualifiedInstance.create(new QualifiedImpl("second"), SecondQualifier.QUALIFIER),
59+
Service.QualifiedInstance.create(new QualifiedImpl("both"),
60+
FirstQualifier.QUALIFIER,
61+
SecondQualifier.QUALIFIER)
62+
);
5863
}
5964
}
6065

6166
@Service.Singleton
62-
@Service.PerInstance(NamedConfig.class)
63-
static class TargetTypeProvider implements TargetType {
64-
private final NamedConfig config;
67+
static class QualifiedReceiver {
68+
private final QualifiedContract first;
69+
private final QualifiedContract second;
70+
private final QualifiedContract third;
6571

6672
@Service.Inject
67-
TargetTypeProvider(@Service.InstanceName String name,
68-
NamedConfig config,
69-
List<ServiceInstance<CharSequence>> emptyList) {
70-
this.config = config;
71-
if (!name.equals(config.name())) {
72-
throw new IllegalStateException("Got name: " + name + " but config is named: " + config.name());
73-
}
73+
QualifiedReceiver(@FirstQualifier QualifiedContract first,
74+
@SecondQualifier QualifiedContract second,
75+
@FirstQualifier @SecondQualifier QualifiedContract third) {
76+
this.first = first;
77+
this.second = second;
78+
this.third = third;
7479
}
7580

76-
@Override
77-
public NamedConfig config() {
78-
return config;
81+
QualifiedContract first() {
82+
return first;
83+
}
84+
85+
QualifiedContract second() {
86+
return second;
87+
}
88+
89+
QualifiedContract third() {
90+
return third;
7991
}
8092
}
8193

82-
static class NamedConfigImpl implements NamedConfig {
94+
static class QualifiedImpl implements QualifiedContract {
8395
private final String name;
8496

85-
NamedConfigImpl(String name) {
97+
QualifiedImpl(String name) {
8698
this.name = name;
8799
}
88100

89101
@Override
90-
public String name() {
102+
public String description() {
91103
return name;
92104
}
93105
}

0 commit comments

Comments
 (0)