Skip to content

Commit 5be43a3

Browse files
committed
Add generation of batch provider
1 parent 8310d89 commit 5be43a3

12 files changed

Lines changed: 349 additions & 5 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Add backend method for backends that do not support direct reloading [#564](https://github.com/nbbrd/java-service-util/issues/564)
1313
- Add service retrieval by ID [#317](https://github.com/nbbrd/java-service-util/issues/317)
1414
- Add custom signature of batch method [#322](https://github.com/nbbrd/java-service-util/issues/322)
15+
- Add generation of batch provider [#161](https://github.com/nbbrd/java-service-util/issues/161)
1516

1617
### Changed
1718

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package nbbrd.service.examples;
2+
3+
import nbbrd.service.ServiceProvider;
4+
5+
import java.util.ServiceLoader;
6+
7+
public class Providers {
8+
9+
public interface FooSPI {
10+
}
11+
12+
public interface BarSPI {
13+
}
14+
15+
// 💡 One provider, one service
16+
@ServiceProvider
17+
public static class FooProvider implements FooSPI {
18+
}
19+
20+
// 💡 One provider, multiple services
21+
@ServiceProvider(FooSPI.class)
22+
@ServiceProvider(BarSPI.class)
23+
public static class FooBarProvider implements FooSPI, BarSPI {
24+
}
25+
26+
public static void main(String[] args) {
27+
// 💡 Get all providers for the services
28+
ServiceLoader.load(FooSPI.class).forEach(System.out::println);
29+
ServiceLoader.load(BarSPI.class).forEach(System.out::println);
30+
}
31+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package nbbrd.service.examples;
2+
3+
import nbbrd.service.Quantifier;
4+
import nbbrd.service.ServiceDefinition;
5+
import nbbrd.service.ServiceProvider;
6+
7+
import java.util.List;
8+
9+
@ServiceDefinition(quantifier = Quantifier.MULTIPLE, batchType = Rule.Batch.class)
10+
public interface Rule {
11+
12+
interface Batch {
13+
List<Rule> getRules();
14+
}
15+
16+
// 💡 Generates the batch class
17+
@ServiceProvider
18+
enum GuidingPrinciples implements Rule {
19+
A, B, C;
20+
}
21+
22+
static void main(String[] args) {
23+
// 💡 Get the batch of rules
24+
RuleLoader.load().forEach(System.out::println);
25+
}
26+
}

‎java-service-processor/src/main/java/internal/nbbrd/service/definition/BatchDefinition.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class BatchDefinition {
3232
@lombok.NonNull
3333
Optional<MethodReturnKind> methodReturnKind;
3434

35-
enum MethodReturnKind {
35+
public enum MethodReturnKind {
3636
/** Method returns {@code Stream<Service>}. */
3737
STREAM,
3838
/** Method returns {@code Collection<Service>}. */
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package internal.nbbrd.service.provider;
2+
3+
import org.jspecify.annotations.Nullable;
4+
5+
import javax.lang.model.element.TypeElement;
6+
import javax.lang.model.type.ArrayType;
7+
import javax.lang.model.type.DeclaredType;
8+
import javax.lang.model.type.TypeKind;
9+
import javax.lang.model.type.TypeMirror;
10+
import javax.lang.model.util.Elements;
11+
import javax.lang.model.util.Types;
12+
import java.util.Collection;
13+
import java.util.Iterator;
14+
import java.util.stream.Stream;
15+
16+
/**
17+
* Helper to resolve batch method return kinds.
18+
*/
19+
@lombok.experimental.UtilityClass
20+
class BatchMethodHelper {
21+
22+
enum ReturnKind {
23+
STREAM, COLLECTION, ITERABLE, ITERATOR, ARRAY
24+
}
25+
26+
static @Nullable ReturnKind resolve(TypeMirror returnType, TypeElement serviceType, Types types, Elements elements) {
27+
DeclaredType streamType = types.getDeclaredType(elements.getTypeElement(Stream.class.getCanonicalName()), serviceType.asType());
28+
if (types.isAssignable(returnType, streamType)) return ReturnKind.STREAM;
29+
DeclaredType collectionType = types.getDeclaredType(elements.getTypeElement(Collection.class.getCanonicalName()), serviceType.asType());
30+
if (types.isAssignable(returnType, collectionType)) return ReturnKind.COLLECTION;
31+
DeclaredType iterableType = types.getDeclaredType(elements.getTypeElement(Iterable.class.getCanonicalName()), serviceType.asType());
32+
if (types.isAssignable(returnType, iterableType)) return ReturnKind.ITERABLE;
33+
DeclaredType iteratorType = types.getDeclaredType(elements.getTypeElement(Iterator.class.getCanonicalName()), serviceType.asType());
34+
if (types.isAssignable(returnType, iteratorType)) return ReturnKind.ITERATOR;
35+
if (returnType.getKind() == TypeKind.ARRAY) {
36+
TypeMirror componentType = ((ArrayType) returnType).getComponentType();
37+
if (types.isAssignable(componentType, types.erasure(serviceType.asType()))) return ReturnKind.ARRAY;
38+
}
39+
return null;
40+
}
41+
}
42+

‎java-service-processor/src/main/java/internal/nbbrd/service/provider/BatchProviderGenerator.java‎

Whitespace-only changes.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package internal.nbbrd.service.provider;
2+
3+
import javax.lang.model.element.TypeElement;
4+
import javax.lang.model.type.TypeMirror;
5+
6+
/**
7+
* Reference to a batch provider that needs to be generated for an enum.
8+
*/
9+
@lombok.Value
10+
class BatchProviderRef {
11+
12+
@lombok.NonNull
13+
TypeElement batchType;
14+
15+
@lombok.NonNull
16+
TypeElement enumProvider;
17+
18+
@lombok.NonNull
19+
TypeElement service;
20+
21+
@lombok.NonNull
22+
String batchMethodName;
23+
24+
@lombok.NonNull
25+
TypeMirror batchMethodReturnType;
26+
}
27+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package internal.nbbrd.service.provider;
2+
3+
import javax.lang.model.element.TypeElement;
4+
5+
/**
6+
* Registration info for a generated batch provider.
7+
*/
8+
@lombok.Value
9+
class BatchProviderRegistration {
10+
11+
@lombok.NonNull
12+
TypeElement batchService;
13+
14+
@lombok.NonNull
15+
String providerClassName;
16+
}
17+

‎java-service-processor/src/main/java/internal/nbbrd/service/provider/ServiceProviderChecker.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ private boolean isValidInstantiator(Instantiator instantiator) {
8181
return true;
8282
case STATIC_METHOD:
8383
return instantiator.getElement().getSimpleName().contentEquals("provider");
84+
case ENUM_FIELD:
85+
return true;
8486
default:
8587
return false;
8688
}

‎java-service-processor/src/main/java/internal/nbbrd/service/provider/ServiceProviderGenerator.java‎

Lines changed: 143 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
package internal.nbbrd.service.provider;
22

3+
import com.squareup.javapoet.*;
34
import internal.nbbrd.service.Instantiator;
45
import internal.nbbrd.service.ProcessorTool;
6+
import internal.nbbrd.service.ProcessorUtil;
7+
import nbbrd.service.ServiceDefinition;
58

69
import javax.annotation.processing.ProcessingEnvironment;
10+
import javax.lang.model.element.ElementKind;
11+
import javax.lang.model.element.ExecutableElement;
12+
import javax.lang.model.element.Modifier;
713
import javax.lang.model.element.TypeElement;
14+
import javax.lang.model.type.TypeMirror;
15+
import javax.lang.model.util.ElementFilter;
816
import javax.lang.model.util.Types;
917
import java.io.IOException;
10-
import java.util.Comparator;
11-
import java.util.List;
12-
import java.util.Map;
18+
import java.util.*;
1319
import java.util.function.Supplier;
1420
import java.util.stream.Collectors;
1521
import java.util.stream.Stream;
@@ -24,7 +30,17 @@ public ServiceProviderGenerator(Supplier<ProcessingEnvironment> envSupplier) {
2430
}
2531

2632
public void generate(List<ProviderRef> annotationRefs) throws IOException {
27-
registerClassPath(annotationRefs, new ClassPathRegistry(getEnv()));
33+
// Generate batch providers for enums and collect their batch service registrations
34+
List<BatchProviderRef> batchRefs = collectBatchProviderRefs(annotationRefs);
35+
List<BatchProviderRegistration> batchRegistrations = generateBatchProviders(batchRefs);
36+
37+
// Filter out enum providers that have batch providers generated
38+
List<ProviderRef> refsToRegister = filterOutEnumsWithBatchProviders(annotationRefs, batchRefs);
39+
40+
// Register in SPI files
41+
ClassPathRegistry classPath = new ClassPathRegistry(getEnv());
42+
registerClassPath(refsToRegister, classPath);
43+
registerBatchProviders(batchRegistrations, classPath);
2844
}
2945

3046
private void registerClassPath(List<ProviderRef> annotationRefs, ClassPathRegistry classPath) throws IOException {
@@ -52,6 +68,129 @@ private List<ProviderRef> generateDelegates(List<ProviderRef> refs) {
5268
return refs;
5369
}
5470

71+
private List<BatchProviderRef> collectBatchProviderRefs(List<ProviderRef> refs) {
72+
List<BatchProviderRef> result = new ArrayList<>();
73+
for (ProviderRef ref : refs) {
74+
if (ref.getProvider().getKind() == ElementKind.ENUM) {
75+
getBatchProviderRef(ref).ifPresent(result::add);
76+
}
77+
}
78+
return result;
79+
}
80+
81+
private List<ProviderRef> filterOutEnumsWithBatchProviders(List<ProviderRef> annotationRefs, List<BatchProviderRef> batchRefs) {
82+
// Create a set of enum providers that have batch providers for quick lookup
83+
Set<TypeElement> enumsWithBatchProviders = batchRefs.stream()
84+
.map(BatchProviderRef::getEnumProvider)
85+
.collect(Collectors.toSet());
86+
87+
// Filter out those enum providers
88+
return annotationRefs.stream()
89+
.filter(ref -> !enumsWithBatchProviders.contains(ref.getProvider()))
90+
.collect(Collectors.toList());
91+
}
92+
93+
private Optional<BatchProviderRef> getBatchProviderRef(ProviderRef ref) {
94+
ServiceDefinition annotation = ref.getService().getAnnotation(ServiceDefinition.class);
95+
if (annotation == null) {
96+
return Optional.empty();
97+
}
98+
99+
TypeMirror batchType = ProcessorUtil.extractResultType(annotation::batchType);
100+
if (batchType.toString().equals(Void.class.getName())) {
101+
return Optional.empty();
102+
}
103+
104+
TypeElement batchTypeElement = getEnv().asTypeElement(batchType);
105+
return findBatchMethod(batchTypeElement, ref.getService())
106+
.map(method -> new BatchProviderRef(
107+
batchTypeElement,
108+
ref.getProvider(),
109+
ref.getService(),
110+
method.getSimpleName().toString(),
111+
method.getReturnType()
112+
));
113+
}
114+
115+
private Optional<ExecutableElement> findBatchMethod(TypeElement batchType, TypeElement serviceType) {
116+
Types types = getEnv().getTypeUtils();
117+
return ElementFilter.methodsIn(batchType.getEnclosedElements())
118+
.stream()
119+
.filter(method -> method.getModifiers().contains(Modifier.PUBLIC))
120+
.filter(method -> !method.getModifiers().contains(Modifier.STATIC))
121+
.filter(method -> method.getParameters().isEmpty())
122+
.filter(method -> isBatchReturnType(method.getReturnType(), serviceType, types))
123+
.findFirst();
124+
}
125+
126+
private boolean isBatchReturnType(TypeMirror returnType, TypeElement serviceType, Types types) {
127+
return BatchMethodHelper.resolve(returnType, serviceType, types, getEnv().getElementUtils()) != null;
128+
}
129+
130+
private List<BatchProviderRegistration> generateBatchProviders(List<BatchProviderRef> batchRefs) {
131+
List<BatchProviderRegistration> result = new ArrayList<>();
132+
for (BatchProviderRef ref : batchRefs) {
133+
String providerClassName = generateBatchProvider(ref);
134+
result.add(new BatchProviderRegistration(ref.getBatchType(), providerClassName));
135+
}
136+
return result;
137+
}
138+
139+
private String generateBatchProvider(BatchProviderRef ref) {
140+
TypeSpec batchImpl = buildBatchClass(ref);
141+
String packageName = getEnv().getElementUtils().getPackageOf(ref.getEnumProvider()).getQualifiedName().toString();
142+
JavaFile javaFile = JavaFile.builder(packageName, batchImpl).build();
143+
ProcessorUtil.write(getEnv(), javaFile);
144+
145+
// Return the fully qualified class name
146+
return packageName.isEmpty()
147+
? ref.getEnumProvider().getSimpleName() + "BatchProvider"
148+
: packageName + "." + ref.getEnumProvider().getSimpleName() + "BatchProvider";
149+
}
150+
151+
private void registerBatchProviders(List<BatchProviderRegistration> registrations, ClassPathRegistry classPath) throws IOException {
152+
for (BatchProviderRegistration registration : registrations) {
153+
List<ProviderConfigurationFileLine> oldLines = classPath.readLinesByService(registration.getBatchService());
154+
List<ProviderConfigurationFileLine> newLines = new ArrayList<>();
155+
newLines.add(ProviderConfigurationFileLine.ofProviderBinaryName(registration.getProviderClassName()));
156+
classPath.writeLinesByService(concat(oldLines, newLines), registration.getBatchService());
157+
}
158+
}
159+
160+
private TypeSpec buildBatchClass(BatchProviderRef ref) {
161+
String className = ref.getEnumProvider().getSimpleName() + "BatchProvider";
162+
ClassName enumName = ClassName.get(ref.getEnumProvider());
163+
ClassName batchName = ClassName.get(ref.getBatchType());
164+
165+
MethodSpec method = MethodSpec.methodBuilder(ref.getBatchMethodName())
166+
.addModifiers(Modifier.PUBLIC)
167+
.addAnnotation(Override.class)
168+
.returns(TypeName.get(ref.getBatchMethodReturnType()))
169+
.addStatement("return $L", buildBatchMethodBody(enumName, ref))
170+
.build();
171+
172+
return TypeSpec.classBuilder(className)
173+
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
174+
.addSuperinterface(batchName)
175+
.addMethod(method)
176+
.build();
177+
}
178+
179+
private CodeBlock buildBatchMethodBody(ClassName enumName, BatchProviderRef ref) {
180+
BatchMethodHelper.ReturnKind kind = BatchMethodHelper.resolve(
181+
ref.getBatchMethodReturnType(), ref.getService(),
182+
getEnv().getTypeUtils(), getEnv().getElementUtils()
183+
);
184+
185+
if (kind == BatchMethodHelper.ReturnKind.STREAM) {
186+
return CodeBlock.of("$T.stream($T.values())", Arrays.class, enumName);
187+
} else if (kind == BatchMethodHelper.ReturnKind.ARRAY) {
188+
return CodeBlock.of("$T.values()", enumName);
189+
} else {
190+
return CodeBlock.of("$T.asList($T.values())", Arrays.class, enumName);
191+
}
192+
}
193+
55194
private static boolean isStaticMethod(Instantiator o) {
56195
return o.getKind() == Instantiator.Kind.STATIC_METHOD;
57196
}

0 commit comments

Comments
 (0)