Skip to content

Commit

Permalink
Add explicit declaration of batch class
Browse files Browse the repository at this point in the history
  • Loading branch information
charphi committed Feb 6, 2024
1 parent 5dbd269 commit 6d43f0e
Show file tree
Hide file tree
Showing 14 changed files with 246 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add mustache templating in loader and batch names [#320](https://github.com/nbbrd/java-service-util/issues/320)
- Add explicit declaration of batch class [#216](https://github.com/nbbrd/java-service-util/issues/216)

### Changed

Expand Down
35 changes: 27 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Features:
- checks coherence of service use in modules if `module-info.java` is available
- allows [identification](#serviceid)
- allows [filtering](#servicefilter) and [sorting](#servicesorter)
- allows [batch loading](#batch-and-batch-name-properties)
- allows [batch loading](#batch-type-property)
- allows [custom backend](#backend-and-cleaner-properties)

Limitations:
Expand All @@ -74,7 +74,7 @@ Main properties:
- [`#quantifier`](#quantifier-property): number of services expected at runtime
- [`#loaderName`](#loader-name-property): custom qualified name of the loader
- [`#fallback`](#fallback-property): fallback type for `SINGLE` quantifier
- [`#batch` `#batchName`](#batch-and-batch-name-properties): bridge different services and generate providers on the fly
- [`#batchType`](#batch-type-property): bridge different services and generate providers on the fly

Advanced properties:
- [`#mutability`](#mutability-property): on-demand set and reload
Expand Down Expand Up @@ -197,12 +197,13 @@ public class NoOpFooProvider implements FooSPI { }
_Note that a warning is raised at compile time if the fallback is missing
but this warning can be disabled with the `@SupressWarning("SingleFallbackNotExpected")` annotation._

#### Batch and batch name properties
#### Batch type property

The `#batch` and `#batchName` properties allow to **bridge different services** and to **generate providers on the fly**.
The `#batchType` property allows to **bridge different services** and to **generate providers on the fly**.
Batch providers are used alongside regular providers.

```java
@ServiceDefinition(batch = true, quantifier = Quantifier.MULTIPLE)
@ServiceDefinition(quantifier = Quantifier.MULTIPLE, batchType = SwingColorScheme.Batch.class)
public interface SwingColorScheme {

List<Color> getColors();
Expand All @@ -215,9 +216,13 @@ public interface SwingColorScheme {
.forEach(System.out::println);
}

interface Batch {
Stream<SwingColorScheme> getProviders();
}

// 💡 Bridge between SwingColorScheme and RgbColorScheme
@ServiceProvider(SwingColorSchemeBatch.class)
final class RgbBridge implements SwingColorSchemeBatch {
@ServiceProvider(Batch.class)
final class RgbBridge implements Batch {

@Override
public Stream<SwingColorScheme> getProviders() {
Expand All @@ -226,10 +231,24 @@ public interface SwingColorScheme {
.map(RgbAdapter::new);
}
}

// 💡 Regular provider
@ServiceProvider(SwingColorScheme.class)
final class Cyan implements SwingColorScheme {

@Override
public List<Color> getColors() {
return Collections.singletonList(Color.CYAN);
}
}
}
```
_Source: [nbbrd/service/examples/SwingColorScheme.java](java-service-examples/src/main/java/nbbrd/service/examples/SwingColorScheme.java)_

Constraints:
1. Batch type must be an interface or an abstract class.
2. Batch method must be unique.

#### Mutability property

The `#mutability` property allows **on-demand set and reload** of a loader.
Expand Down Expand Up @@ -279,7 +298,7 @@ Properties:
- `#pattern`: specifies the regex pattern that the ID is expected to match

```java
@ServiceDefinition(quantifier = Quantifier.MULTIPLE, batch = true)
@ServiceDefinition(quantifier = Quantifier.MULTIPLE)
public interface HashAlgorithm {

// 💡 Enforce service naming
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@
* Specifies if batch loading should be allowed.
*
* @return true if batch loading should be allowed, false otherwise
* @deprecated use {@link #batchType()} instead
*/
@Deprecated
boolean batch() default false;

/**
Expand All @@ -199,9 +201,24 @@
* </ul>
*
* @return a fully qualified name
* @deprecated use {@link #batchType()} instead
*/
@Deprecated
String batchName() default "";

/**
* Specifies the batch class to use in batch loading.
* <p>
* Requirements:
* <ol>
* <li>Batch type must be an interface or an abstract class</li>
* <li>Batch method must be unique</li>
* </ol>
*
* @return the batch class if required, {@link Void} otherwise
*/
Class<?> batchType() default Void.class;

@SuppressWarnings("rawtypes")
final class NoProcessing implements UnaryOperator<Stream> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import static java.nio.charset.StandardCharsets.UTF_8;

@ServiceDefinition(quantifier = Quantifier.MULTIPLE, batch = true)
@ServiceDefinition(quantifier = Quantifier.MULTIPLE, batchType = HashAlgorithm.Batch.class)
public interface HashAlgorithm {

// 💡 Enforce service naming
Expand All @@ -33,8 +33,12 @@ static void main(String[] args) {
.ifPresent(System.out::println);
}

interface Batch {
Stream<HashAlgorithm> getProviders();
}

@ServiceProvider
class MessageDigestBridge implements HashAlgorithmBatch {
class MessageDigestBridge implements Batch {

@Override
public Stream<HashAlgorithm> getProviders() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
import nbbrd.service.ServiceProvider;

import java.awt.*;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@ServiceDefinition(batch = true, quantifier = Quantifier.MULTIPLE)
@ServiceDefinition(quantifier = Quantifier.MULTIPLE, batchType = SwingColorScheme.Batch.class)
public interface SwingColorScheme {

List<Color> getColors();
Expand All @@ -22,9 +23,13 @@ static void main(String[] args) {
.forEach(System.out::println);
}

interface Batch {
Stream<SwingColorScheme> getProviders();
}

// 💡 Bridge between SwingColorScheme and RgbColorScheme
@ServiceProvider(SwingColorSchemeBatch.class)
final class RgbBridge implements SwingColorSchemeBatch {
@ServiceProvider(Batch.class)
final class RgbBridge implements Batch {

@Override
public Stream<SwingColorScheme> getProviders() {
Expand All @@ -34,6 +39,16 @@ public Stream<SwingColorScheme> getProviders() {
}
}

// 💡 Regular provider
@ServiceProvider(SwingColorScheme.class)
final class Cyan implements SwingColorScheme {

@Override
public List<Color> getColors() {
return Collections.singletonList(Color.CYAN);
}
}

final class RgbAdapter implements SwingColorScheme {

private final RgbColorScheme rgb;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class LoadDefinition {
@lombok.NonNull
String batchName;

@lombok.NonNull
Optional<TypeMirror> batchType;

public @NonNull ClassName resolveLoaderName() {
return resolveName(loaderName, serviceType, "Loader");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,30 @@
import nbbrd.service.ServiceDefinition;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import java.io.IOException;
import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Stream;

import static javax.lang.model.element.Modifier.*;

/**
* @author Philippe Charles
*/
@SuppressWarnings({"OptionalUsedAsFieldOrParameterType", "RedundantIfStatement"})
final class ServiceDefinitionChecker {

private final ExtEnvironment env;
Expand Down Expand Up @@ -180,7 +188,10 @@ public boolean checkDefinition(LoadDefinition definition) {
if (!checkCleaner(definition.getCleaner(), service, types)) {
return false;
}
if (!checkMutability(definition, service, types)) {
if (!checkMutability(definition, service)) {
return false;
}
if (!checkBatch(definition, service)) {
return false;
}
return true;
Expand Down Expand Up @@ -309,13 +320,42 @@ private boolean checkCleanerTypeHandler(TypeInstantiator handler, TypeElement se
return true;
}

private boolean checkMutability(LoadDefinition definition, TypeElement service, Types types) {
private boolean checkMutability(LoadDefinition definition, TypeElement service) {
if (definition.getLifecycle() == Lifecycle.UNSAFE_MUTABLE) {
env.warn(service, String.format(Locale.ROOT, "Thread-unsafe singleton for '%1$s'", service));
}
return true;
}

private boolean checkBatch(LoadDefinition definition, TypeElement service) {
if (definition.getBatchType().isPresent()) {
if (definition.isBatch()) {
env.error(service, "Batch type cannot be used with batch property");
return false;
}
TypeElement x = env.asTypeElement(definition.getBatchType().get());
if (x.getKind() != ElementKind.INTERFACE && !x.getModifiers().contains(ABSTRACT)) {
env.error(service, "[RULE_B1] Batch type must be an interface or an abstract class");
return false;
}
if (ElementFilter.methodsIn(x.getEnclosedElements()).stream().filter(batchMethodFilter(service)).count() != 1) {
env.error(service, "[RULE_B2] Batch method must be unique");
return false;
}
}
return true;
}

private Predicate<ExecutableElement> batchMethodFilter(TypeElement service) {
DeclaredType streamType = env.getTypeUtils().getDeclaredType(env.asTypeElement(Stream.class), service.asType());
return method -> method.getModifiers().contains(PUBLIC)
&& !method.getModifiers().contains(STATIC)
&& method.getParameters().isEmpty()
&& method.getSimpleName().contentEquals("getProviders")
&& env.getTypeUtils().isAssignable(method.getReturnType(), streamType)
&& !hasCheckedExceptions(method);
}

private boolean checkInstanceFactories(TypeElement annotatedElement, TypeMirror type, TypeInstantiator instance) {
if (!instance.select().isPresent()) {
env.error(annotatedElement, String.format(Locale.ROOT, "Don't know how to instantiate '%1$s'", type));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ private LoadDefinition definitionOf(TypeElement serviceType) {
Optional<TypeInstantiator> cleaner = nonNull(annotation::cleaner, ServiceDefinition.DefaultCleaner.class)
.map(type -> new TypeInstantiator(type, Instantiator.allOf(types, env.asTypeElement(type), env.asTypeElement(type))));

Optional<TypeMirror> batchType = nonNull(annotation::batchType, Void.class);

return LoadDefinition
.builder()
.quantifier(annotation.quantifier())
Expand All @@ -129,6 +131,7 @@ private LoadDefinition definitionOf(TypeElement serviceType) {
.cleaner(cleaner)
.batch(annotation.batch())
.batchName(annotation.batchName())
.batchType(batchType)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,14 +424,24 @@ private FieldSpec getSourceField() {
}

private Optional<FieldSpec> getBatchField() {
return definition.isBatch()
? Optional.of(FieldSpec
.builder(typeOf(Iterable.class, definition.resolveBatchName()), fieldName("batch"))
.addModifiers(PRIVATE, FINAL)
.addModifiers(getSingletonModifiers())
.initializer("$L", getBackendInitCode(definition.resolveBatchName()))
.build())
: Optional.empty();
if (definition.isBatch()) {
return Optional.of(FieldSpec
.builder(typeOf(Iterable.class, definition.resolveBatchName()), fieldName("batch"))
.addModifiers(PRIVATE, FINAL)
.addModifiers(getSingletonModifiers())
.initializer("$L", getBackendInitCode(definition.resolveBatchName()))
.build());
}
if (definition.getBatchType().isPresent()) {
ClassName batchTypeName = ClassName.bestGuess(definition.getBatchType().get().toString());
return Optional.of(FieldSpec
.builder(typeOf(Iterable.class, batchTypeName), fieldName("batch"))
.addModifiers(PRIVATE, FINAL)
.addModifiers(getSingletonModifiers())
.initializer("$L", getBackendInitCode(batchTypeName))
.build());
}
return Optional.empty();
}

@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
Expand Down
Loading

0 comments on commit 6d43f0e

Please sign in to comment.