ProcessServiceBuildStepProcessor.java

package io.vanillabp.integration.deployment.processservice;

import static io.quarkus.gizmo.Type.classType;
import static io.quarkus.gizmo.Type.parameterizedType;

import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.Type;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.SignatureBuilder;
import io.vanillabp.integration.deployment.config.MigrationAdapterPropertiesBuildItem;
import io.vanillabp.integration.deployment.validation.EnsureClassIsBeanValidationBuildItem;
import io.vanillabp.integration.deployment.workflowmodule.VanillaBpWorkflowModulesBuildItem;
import io.vanillabp.integration.deployment.workflowmodule.WorkflowModuleBuildStepProcessor;
import io.vanillabp.integration.runtime.processservice.ProcessServiceBaseCdiBean;
import io.vanillabp.integration.spi.AggregatePersistenceAware;
import io.vanillabp.spi.process.ProcessService;
import io.vanillabp.spi.service.WorkflowService;
import jakarta.enterprise.context.ApplicationScoped;
import lombok.extern.slf4j.Slf4j;

/**
 * VanillaBP extension build step processor, responsible for building {@link ProcessService} beans.
 */
@Slf4j
public class ProcessServiceBuildStepProcessor {

  public static String ANNOTATION_WORKFLOWSERVICE_ATTRIBUTE_AGGREGATECLASS = "workflowAggregateClass";
  public static String ANNOTATION_WORKFLOWSERVICE_ATTRIBUTE_BPMNPROCESS = "bpmnProcess";
  public static String ANNOTATION_WORKFLOWSERVICE_ATTRIBUTE_BPMNPROCESS_BPMNPROCESSID = "bpmnProcessId";

  /**
   * Build step for build {@link ProcessService} beans for all services
   * annotated by {@link WorkflowService} at class level.
   *
   * @param applicationArchivesBuildItem Information about All archives (JARs and directories) of the project
   * @param migrationAdapterProperties Properties of the migration adapter previously built and validated as a dependency
   * @param workflowModulesFound Information about all workflow modules found in the project
   * @param generatedBeanBuildItemBuildProducer {@link BuildProducer} used to collect generated {@link ProcessService} beans
   * @param additionalBeanBuildItemBuildProducer {@link BuildProducer} used to collect beans provided in module "runtime"
   */
  @BuildStep
  void buildProcessServices(
      final ApplicationArchivesBuildItem applicationArchivesBuildItem,
      final MigrationAdapterPropertiesBuildItem migrationAdapterProperties,
      final VanillaBpWorkflowModulesBuildItem workflowModulesFound,
      final BuildProducer<EnsureClassIsBeanValidationBuildItem> ensureClassIsBeanBuildItemProducer,
      final BuildProducer<GeneratedBeanBuildItem> generatedBeanBuildItemBuildProducer,
      final BuildProducer<AdditionalBeanBuildItem> additionalBeanBuildItemBuildProducer) {

    final var aggregatePersistenceAwares = applicationArchivesBuildItem
        // search all archives of the project
        .getAllApplicationArchives()
        .stream()
        .flatMap(archive -> archive
            // collect each archive's known implementations
            .getIndex()
            .getAllKnownImplementations(AggregatePersistenceAware.class)
            .stream()
            .map(aware -> Map.entry(aware, archive)))
        .toList();

    // scan for classes annotated by @WorkflowService
    final Set<Type> processServicesBuilt = new HashSet<>();
    applicationArchivesBuildItem
        // search all archives of the project
        .getAllApplicationArchives()
        .stream()
        .flatMap(archive -> archive
            .getIndex()
            .getAnnotations(WorkflowService.class)
            .stream())
        // and build an adapter-aware process service for each workflow aggregate class
        .forEach(annotation -> {

          final var workflowAggregateType = annotation
              .value(ANNOTATION_WORKFLOWSERVICE_ATTRIBUTE_AGGREGATECLASS)
              .asClass();
          // if there is more than one @WorkflowService class for a specific BPMN process ID,
          // then use the one previously built
          if (processServicesBuilt.contains(workflowAggregateType)) {
            return;
          }

          // collect information necessary for bean creation
          final var serviceClass = annotation.target().asClass();
          // ensure service class will be a CDI bean at runtime
          ensureClassIsBeanBuildItemProducer
              .produce(EnsureClassIsBeanValidationBuildItem
                  .builder()
                  .className(serviceClass.name())
                  .usageDescription("Workflow service annotated with @"
                      + WorkflowService.class.getName())
                  .build());

          final var workflowModuleId = WorkflowModuleBuildStepProcessor
              .getWorkflowModuleId(
                  workflowModulesFound,
                  applicationArchivesBuildItem,
                  serviceClass);
          final var bpmnProcessId = Optional
              .ofNullable(annotation.value(ANNOTATION_WORKFLOWSERVICE_ATTRIBUTE_BPMNPROCESS))
              .map(AnnotationValue::asNested)
              .map(a -> a.value(ANNOTATION_WORKFLOWSERVICE_ATTRIBUTE_BPMNPROCESS_BPMNPROCESSID))
              .map(AnnotationValue::asString)
              .orElse(serviceClass.simpleName());

          // find persistence support class for the aggregate class
          final var aggregatePersistenceType = aggregatePersistenceAwares
              .stream()
              // calculate distance of classes
              .map(awareEntry -> Map.entry(
                  awareEntry.getKey(),
                  AggregatePersistenceResolver.distance(
                      awareEntry.getValue().getIndex(),
                      awareEntry.getKey(),
                      workflowAggregateType.name()
                  )))
              // filter persistence awares those aggregate type is not assignable to the current aggregate type
              .filter(awareEntry -> awareEntry.getValue() != Integer.MAX_VALUE)
              // choose the most specific persistence support in terms of inheritance class distance
              .min(Comparator.comparingInt(Map.Entry::getValue))
              // if none found, fall back to persistence support based on Spring Data Util bean
              .map(Map.Entry::getKey)
              .orElseThrow(() -> new IllegalStateException(
                  "You have to provide a CDI bean implementing\n  "
                      + AggregatePersistenceAware.class.getName()
                      + "\nwhich is responsible to persist aggregates.\n"
                      + "This is necessary because in Quarkus there is no unique way to do persistence of entities:\n"
                      + "- Active record pattern: https://quarkus.io/guides/hibernate-orm-panache#solution-1-using-the-active-record-pattern\n"
                      + "- Repository record pattern: https://quarkus.io/guides/hibernate-orm-panache#solution-2-using-the-repository-pattern\n"
                      + "- Spring Data pattern: https://quarkus.io/guides/spring-data-jpa"
              ));
          // ensure service class will be a CDI bean at runtime
          ensureClassIsBeanBuildItemProducer
              .produce(EnsureClassIsBeanValidationBuildItem
                  .builder()
                  .className(aggregatePersistenceType.name())
                  .usageDescription("Service implementing the interface "
                      + AggregatePersistenceAware.class.getName())
                  .build());

          // generate process service CDI bean specific to the workflow aggregate
          generateProcessService(
              generatedBeanBuildItemBuildProducer,
              workflowModuleId,
              bpmnProcessId,
              "%s.ProcessService_%s".formatted(
                  workflowAggregateType.name().packagePrefix(),
                  workflowAggregateType.name().withoutPackagePrefix()),
              aggregatePersistenceType,
              workflowAggregateType);

          // prevent building more than one process service even if a workflow aggregate class
          // is used in more than one @WorkflowService annotated service
          processServicesBuilt.add(workflowAggregateType);

        });

  }

  /**
   * Generate process service CDI bean specific to the workflow aggregate type given.
   *
   * @param generatedBeanBuildItemBuildProducer The producer used to build multiple beans if necessary
   * @param workflowModuleId The ID of the workflow module the service belongs to
   * @param bpmnProcessId The BPMN process ID the service is for
   * @param className The class name of the service to build
   * @param aggregatePersistenceType The aggregate persistence type to be used by the service
   * @param workflowAggregateType The workflow aggregate type the service is for
   */
  private void generateProcessService(
      final BuildProducer<GeneratedBeanBuildItem> generatedBeanBuildItemBuildProducer,
      final String workflowModuleId,
      final String bpmnProcessId,
      final String className,
      final ClassInfo aggregatePersistenceType,
      final Type workflowAggregateType) {

    final var aggregatePersistenceClassName = aggregatePersistenceType.name().toString();
    final var aggregateClassName = workflowAggregateType.name().toString();

    /*
     * public class ProcessService_Aggregate extends ProcessServiceBaseCdiBean<Aggregate> {
     */
    final var beanClassOutput = new GeneratedBeanGizmoAdaptor(generatedBeanBuildItemBuildProducer);
    final var cc = ClassCreator
        .builder()
        .classOutput(beanClassOutput)
        .className(className)
        .signature(SignatureBuilder
            .forClass()
            .setSuperClass(
                parameterizedType(classType(ProcessServiceBaseCdiBean.class), classType(workflowAggregateType.name()))))
        .build();

    // @ApplicationScoped
    cc.addAnnotation(ApplicationScoped.class);

    /*
     * Class<AggregatePersistenceAware<A>> getAggregatePersistenceClass()
     */
    final var getAggregatePersistenceClass = cc.getMethodCreator(
        "getAggregatePersistenceClass",
        Class.class
    );
    // return AggregatePersistence.class;
    getAggregatePersistenceClass
        .returnValue(getAggregatePersistenceClass.loadClass(aggregatePersistenceClassName));

    /*
     * Class<A> getWorkflowAggregateClass()
     */
    final var getAggregateClass = cc.getMethodCreator(
        "getWorkflowAggregateClass",
        Class.class
    );
    // return A.class;
    getAggregateClass.returnValue(
        getAggregateClass.loadClass(aggregateClassName));

    /*
     * String getWorkflowModuleId()
     */
    final var getWorkflowModuleId = cc.getMethodCreator(
        "getWorkflowModuleId",
        String.class);
    // return "wmid";
    getWorkflowModuleId.returnValue(
        getWorkflowModuleId.load(workflowModuleId));

    /*
     * String getBpmnProcessId()
     */
    final var getBpmnProcessId = cc.getMethodCreator(
        "getBpmnProcessId",
        String.class);
    // return "pid";
    getBpmnProcessId.returnValue(
        getBpmnProcessId.load(bpmnProcessId));

    cc.close();

  }

}