io.helidon.microprofile.lra.ParticipantValidationModel Maven / Gradle / Ivy
/*
* Copyright (c) 2021, 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.microprofile.lra;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.helidon.common.LazyValue;
import io.helidon.common.reactive.Single;
import jakarta.enterprise.inject.spi.DeploymentException;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.lra.annotation.ParticipantStatus;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
class ParticipantValidationModel {
private static final Set PARTICIPANT_ANNOTATIONS = Set.of(
InspectionService.AFTER_LRA,
InspectionService.COMPENSATE,
InspectionService.COMPLETE,
InspectionService.FORGET,
InspectionService.STATUS
);
private final ClassInfo classInfo;
private final Map methods = new HashMap<>();
ParticipantValidationModel(ClassInfo classInfo) {
this.classInfo = classInfo;
}
ParticipantValidationModel method(MethodInfo method, Set annotations) {
ParticipantMethod pm = new ParticipantMethod(method).annotations(annotations);
methods.putIfAbsent(pm.nameWithParams(), pm);
methods.computeIfPresent(pm.nameWithParams(), (k, v) -> v.annotations(annotations));
return this;
}
boolean isParticipant() {
return !methods.isEmpty();
}
private void validateMandatoryMethods() {
// one of compensate or afterLra is mandatory
Set mandatoryAnnotations = Set.of(InspectionService.COMPENSATE, InspectionService.AFTER_LRA);
if (methods.values().stream()
.flatMap(ParticipantValidationModel.ParticipantMethod::annotationStream)
.map(AnnotationInstance::name)
.noneMatch(mandatoryAnnotations::contains)) {
throw new DeploymentException("Missing @Compensate or @AfterLRA on class " + classInfo);
}
}
private void validateAmbiguousCompensatorMethods() {
Map> groupedParticipantMethods = methods.values().stream()
.filter(ParticipantMethod::isCompensator)
.collect(Collectors.groupingBy(pm -> pm.participantAnnotation().get()));
for (Map.Entry> e : groupedParticipantMethods.entrySet()) {
int size = e.getValue().size();
if (size > 1) {
throw new DeploymentException("Class " + classInfo.simpleName()
+ " have " + size
+ " methods with annotation @" + e.getKey().withoutPackagePrefix()
+ ": " + e.getValue().stream().map(pm -> pm.method().toString()).collect(Collectors.joining(", ")));
}
}
}
private void validateReturnTypes() {
//allowed return types for compensate and afterLra
Set returnTypes = Stream.of(
Response.class,
ParticipantStatus.class,
CompletionStage.class,
CompletableFuture.class,
Single.class,
void.class
)
.map(Class::getName)
.map(DotName::createSimple)
.collect(Collectors.toSet());
methods.forEach((m, a) -> {
if (a.annotationStream().map(AnnotationInstance::name).anyMatch(InspectionService.COMPENSATE::equals)) {
if (!returnTypes.contains(a.method().returnType().name())) {
throw new DeploymentException("Invalid return type " + a.method().returnType()
+ " of compensating method " + m);
}
}
if (a.annotationStream().map(AnnotationInstance::name).anyMatch(InspectionService.AFTER_LRA::equals)) {
if (!returnTypes.contains(a.method().returnType().name())) {
throw new DeploymentException("Invalid return type "
+ a.method().returnType() + " of after method "
+ m);
}
}
});
}
void validate() {
validateMandatoryMethods();
validateAmbiguousCompensatorMethods();
validateReturnTypes();
}
private static class ParticipantMethod {
private final MethodInfo methodInfo;
private final Set annotations = new HashSet<>();
private final Set annotationTypes = new HashSet<>();
private final LazyValue> participantAnnotation = LazyValue.create(() -> {
List dotNames = annotationTypes.stream()
.filter(PARTICIPANT_ANNOTATIONS::contains)
.collect(Collectors.toList());
if (dotNames.size() > 1) {
throw new DeploymentException("Participant method "
+ this.nameWithParams()
+ " needs to have exactly one compensator annotation but has "
+ dotNames.stream().map(DotName::withoutPackagePrefix).collect(Collectors.joining(", "))
);
}
return dotNames.stream().findFirst();
});
ParticipantMethod(MethodInfo methodInfo) {
this.methodInfo = methodInfo;
}
String nameWithParams() {
return methodInfo.name() + methodInfo.parameterTypes().stream()
.map(Type::name)
.map(DotName::toString)
.collect(Collectors.joining());
}
ParticipantMethod annotations(Collection a) {
this.annotations.addAll(a);
a.forEach(ai -> annotationTypes.add(ai.name()));
return this;
}
Stream annotationStream() {
return annotations.stream();
}
/**
* Any LRA method except @LRA.
*
* @return true if so
*/
boolean isCompensator() {
return participantAnnotation().isPresent();
}
Optional participantAnnotation() {
return participantAnnotation.get();
}
MethodInfo method() {
return methodInfo;
}
}
}