VanillaBpIntegrationProcessor.java

package io.vanillabp.integration.deployment;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.jboss.jandex.AnnotationTransformation;
import org.jboss.jandex.DotName;
import org.jboss.jandex.ParameterizedType;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
import io.quarkus.arc.deployment.InterceptorBindingRegistrarBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.processor.InterceptorBindingRegistrar;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.StaticInitConfigBuilderBuildItem;
import io.vanillabp.integration.deployment.config.QuarkusMigrationAdapterProperties;
import io.vanillabp.integration.deployment.config.QuarkusMigrationAdapterTransformer;
import io.vanillabp.integration.deployment.processservice.VanillaBpMigratableProcessServiceBuildItem;
import io.vanillabp.integration.runtime.processservice.ProcessServiceCdiBeanRecorder;
import io.vanillabp.integration.runtime.processservice.TransactionInterceptor;
import io.vanillabp.integration.runtime.processservice.config.QuarkusMigrationAdapterPropertiesBuilder;
import io.vanillabp.spi.process.ProcessService;
import io.vanillabp.spi.service.WorkflowService;
import io.vanillabp.spi.service.WorkflowTask;
import jakarta.inject.Singleton;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class VanillaBpIntegrationProcessor {

  /**
   * Each VanillaBP adapter is a Quarkus extension publishing a Quarkus extension
   * capability with its name prefix by this prefix. e.g. io.vanillabp.adapter.dummy
   */
  public static final String PREFIX_ADAPTER_PACKAGE = "io.vanillabp.adapter.";

  private static final String FEATURE = "vanillabp";

  public static String ANNOTATION_WORKFLOWSERVICE_ATTRIBUTE_AGGREGATECLASS = "workflowAggregateClass";

  /**
   * Use customized builder for migration adapter properties.
   *
   * @return Build item for migration adapter properties
   */
  @BuildStep
  StaticInitConfigBuilderBuildItem buildMigrationAdapterProperties() {

    return new StaticInitConfigBuilderBuildItem(QuarkusMigrationAdapterPropertiesBuilder.class);

  }

  /**
   * Build step for introducing {@link TransactionInterceptor} for all method's
   * annotated by @{@link WorkflowTask}.
   *
   * @param annotationsTransformer {@link TransactionInterceptor}'s annotations need be transformed
   * @param interceptorBindingRegistrarProducer @{@link WorkflowTask} needs to be registered manually
   * @return The additional {@link TransactionInterceptor} bean
   */
  @BuildStep
  AdditionalBeanBuildItem buildTransactionInterceptors(
      final BuildProducer<AnnotationsTransformerBuildItem> annotationsTransformer,
      final BuildProducer<InterceptorBindingRegistrarBuildItem> interceptorBindingRegistrarProducer) {

    // Typically an Interceptor needs an Annotation for interceptor binding. Since the
    // annotation @WorkflowTask it is used for is not an interceptor binding annotation
    // it needs to be added programmatically:
    annotationsTransformer.produce(new AnnotationsTransformerBuildItem(AnnotationTransformation
        .forClasses()
        .whenClass(DotName.createSimple(TransactionInterceptor.class.getName()))
        .transform(t -> t.add(WorkflowTask.class))));

    final var annotationMethods = Arrays
        .stream(WorkflowTask.class.getDeclaredMethods())
        .map(Method::getName)
        .collect(Collectors.toSet());
    // Typically an Interceptor needs an Annotation for interceptor binding. Since the
    // annotation @WorkflowTask it is used for is not an interceptor binding annotation
    // the interceptor binding needs to be added programmatically:
    interceptorBindingRegistrarProducer.produce(new InterceptorBindingRegistrarBuildItem(
        new InterceptorBindingRegistrar() {
          @Override
          public List<InterceptorBinding> getAdditionalBindings() {
            return List.of(InterceptorBinding.of(
                WorkflowTask.class,
                // all annotation-values need to be ignored to run the interceptor
                // regardless the value of the annotation
                annotationMethods
            ));
          }
        }
    ));

    // Beans of runtime package need to be registered as additional bean to the index:
    return AdditionalBeanBuildItem
        .builder()
        .addBeanClass(TransactionInterceptor.class)
        .setUnremovable() // don't remove, since it is used under the hoods
        .build();

  }

  @Record(ExecutionTime.STATIC_INIT)
  @BuildStep
  void buildProcessServices(
      final QuarkusMigrationAdapterProperties properties,
      final Capabilities capabilities,
      final BeanArchiveIndexBuildItem indexBuildItem,
      final BuildProducer<FeatureBuildItem> featureProducer,
      final ProcessServiceCdiBeanRecorder processServiceRecorder,
      final List<VanillaBpMigratableProcessServiceBuildItem> processServiceBuildItems,
      final BuildProducer<SyntheticBeanBuildItem> syntheticBeanProducer/* ,
                                                                       final BeanContainer beanContainer */) {

    featureProducer.produce(new FeatureBuildItem(FEATURE));

    // check for consistent configuration and required VanillaBpMigratableProcessServiceBuildItems
    final var adapterProperties = QuarkusMigrationAdapterTransformer
        .builder()
        .properties(properties)
        .capabilities(capabilities)
        .build()
        .getAndValidatePropertiesConfigured(processServiceBuildItems);

    // scan for bean annotated by @WorkflowService
    final Set<Class<?>> processServicesBuilt = new HashSet<>();
    indexBuildItem
        .getIndex()
        .getAnnotations(WorkflowService.class)
        // and build an adapter aware process service for each of them
        .forEach(annotation -> {
          try {
            final var serviceClass = annotation.target();
            final var workflowAggregateType = annotation.value(ANNOTATION_WORKFLOWSERVICE_ATTRIBUTE_AGGREGATECLASS)
                .asClass();
            final var workflowAggregateClass = getClass().getClassLoader()
                .loadClass(workflowAggregateType.name().toString());
            if (processServicesBuilt.contains(workflowAggregateClass)) {
              return;
            }
            syntheticBeanProducer.produce(SyntheticBeanBuildItem
                .configure(ProcessService.class)
                .types(ParameterizedType.create(ProcessService.class, workflowAggregateType))
                .scope(Singleton.class)
                .supplier(processServiceRecorder.processServiceSupplier(
                    workflowAggregateClass,
                    adapterProperties,
                    // TODO Fill services argument
                    List.of()))
                .done());
            processServicesBuilt.add(workflowAggregateClass);
          } catch (ClassNotFoundException e) {
            log.debug("NoClassDefFoundError: it might be an optional dependency", e);
          }
        });

  }

}