SpringBootMigrationAdapterAutoConfiguration.java

package io.vanillabp.integration.processservice;

import java.util.Comparator;
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.function.Predicate;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ResolvableType;

import io.vanillabp.integration.adapter.AdapterConfigurationBase;
import io.vanillabp.integration.adapter.migration.config.MigrationAdapterProperties;
import io.vanillabp.integration.adapter.migration.processervice.MigrationProcessService;
import io.vanillabp.integration.config.SpringBootMigrationAdapterProperties;
import io.vanillabp.integration.config.SpringBootMigrationAdapterTransformer;
import io.vanillabp.integration.spi.aggregate.AggregatePersistenceAware;
import io.vanillabp.integration.utils.ClasspathScanner;
import io.vanillabp.integration.utils.SpringDataUtil;
import io.vanillabp.integration.utils.impl.SpringDataUtilBasedAggregatePersistenceSupport;
import io.vanillabp.integration.workflowmodule.WorkflowModule;
import io.vanillabp.integration.workflowmodule.WorkflowModuleAutoConfiguration;
import io.vanillabp.integration.workflowmodule.WorkflowModules;
import io.vanillabp.intergration.adapter.spi.MigratableProcessService;
import io.vanillabp.spi.service.WorkflowService;
import lombok.extern.slf4j.Slf4j;

/**
 * Autoconfiguration of VanillaBP adapters.
 */
@Slf4j
@Configuration
@AutoConfigureAfter({
    WorkflowModuleAutoConfiguration.class
})
@EnableConfigurationProperties(SpringBootMigrationAdapterProperties.class)
public class SpringBootMigrationAdapterAutoConfiguration {

  static final String BEANNAME_MIGRATIONADAPERPROPERTIES = "VanillaBpMigrationAdapterProperties";

  private final Map<Class<?>, MigrationProcessService<?>> connectableServices = new HashMap<>();

  /**
   * Maps and validates VanillaBP properties (specific to Spring Boot) to
   * {@link MigrationAdapterProperties} bean. It is used by common adapter
   * implementation of module "migration-adapter".
   *
   * @param properties The Spring Boot specific properties
   * @param allWorkflowModules All workflow modules found in classpath
   * @param adapterConfigurations Configuration beans of adapters found in classpath
   * @return The properties bean not specific to Spring Boot
   */
  @Bean(BEANNAME_MIGRATIONADAPERPROPERTIES)
  public static MigrationAdapterProperties migrationAdapterProperties(
      final SpringBootMigrationAdapterProperties properties,
      final WorkflowModules allWorkflowModules,
      final List<AdapterConfigurationBase> adapterConfigurations) {

    final var adaptersLoaded = Optional
        .ofNullable(adapterConfigurations)
        .orElse(List.of())
        .stream()
        .map(AdapterConfigurationBase::getAdapterType)
        .toList();

    final var workflowModuleIds = allWorkflowModules
        .getWorkflowModules()
        .stream()
        .map(WorkflowModule::getId)
        .toList();

    return SpringBootMigrationAdapterTransformer
        .builder()
        .properties(properties)
        .adaptersFound(adaptersLoaded)
        .workflowModulesFound(workflowModuleIds)
        .build()
        .getAndValidatePropertiesConfigured();

  }

  /**
   * Builds {@link io.vanillabp.spi.process.ProcessService} beans for each
   * aggregate type of workflow services found in classpath.
   *
   * @param allWorkflowModules All workflow modules found in classpath
   * @return A {@link BeanDefinitionRegistryPostProcessor} adding all {@link io.vanillabp.spi.process.ProcessService} beans necessary
   */
  @Bean
  public static BeanDefinitionRegistryPostProcessor buildProcessServices(
      final WorkflowModules allWorkflowModules,
      final Optional<SpringDataUtil> springDataUtil,
      final List<AggregatePersistenceAware<?>> aggregatePersistenceAwares,
      final List<MigratableProcessService<?>> migratableProcessServices) {

    return registry -> {

      try {

        // find all workflow service classes in classpath
        final var workflowServiceClasses = ClasspathScanner
            // find classes annotated with @WorkflowService
            .allClasses(
                "",
                metadataReader -> {
                  try {
                    return metadataReader.getAnnotationMetadata().hasAnnotation(WorkflowService.class.getName());
                  } catch (Exception e) {
                    return false;
                  }
                }
            );

        // associate workflow services with workflow modules
        WorkflowModuleAutoConfiguration.registerProcessServices(
            allWorkflowModules.getWorkflowModules(),
            workflowServiceClasses);

        // build ProcessService<A> beans
        final Set<Class<?>> processServicesBuilt = new HashSet<>();
        workflowServiceClasses
            .forEach(serviceClass -> {
              final var annotation = serviceClass.getAnnotation(WorkflowService.class);
              final var workflowAggregateType = annotation.workflowAggregateClass();

              // 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;
              }

              if (springDataUtil.isEmpty()) {
                throw new IllegalStateException(
                    """
                        Spring Data Util bean not found! To solve this either
                        - add spring-boot-starter-data-jpa to classpath and configure a data source, if you use JPA for persistence of aggregates
                        - add @Import(io.vanillabp.integration.utils.impl.MongoDbSpringDataUtilConfiguration) to your main application class, if you use MongoDb for persistence of aggregates
                        - add your own implementation of io.vanillabp.integration.utils.SpringDataUtil, if you use an alternative persistence""");
              }

              // find persistence support class for the aggregate class
              @SuppressWarnings({
                  "rawtypes", "unchecked"
              })
              final var aggregatePersistenceAware = aggregatePersistenceAwares
                  .stream()
                  // calculate distance of classes
                  .map(aware -> Map.entry(
                      aware,
                      AggregatePersistenceResolver.inheritanceDistance(
                          aware.getAggregateClass(),
                          workflowAggregateType
                      )))
                  // 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)
                  .orElse(new SpringDataUtilBasedAggregatePersistenceSupport(
                      springDataUtil.get(), workflowAggregateType));

              // collect information necessary for bean creation
              final var workflowModuleId = allWorkflowModules
                  .getWorkflowModules()
                  .stream()
                  .filter(workflowModule -> workflowModule.isWorkflowServiceKnown(serviceClass))
                  .findFirst()
                  .map(WorkflowModule::getId)
                  .orElseThrow();
              final var bpmProcessId = Optional.of(annotation
                  .bpmnProcess()
                  .bpmnProcessId())
                  .filter(Predicate.not(String::isEmpty))
                  .orElse(serviceClass.getSimpleName());

              // build bean via bean definition: This means to build it as late as possible.
              // A bean definition is a promise to Spring that this bean will be available and
              // can be used for wiring before the bean is created.
              // The reason for this is to avoid circular dependencies between the class
              // annotated by @WorkflowService and the ProcessService bean.
              final var processServiceBeanDefinition = (RootBeanDefinition) BeanDefinitionBuilder
                  .rootBeanDefinition(ProcessServiceSpringBean.class)
                  .addConstructorArgValue(workflowModuleId)
                  .addConstructorArgValue(bpmProcessId)
                  .addConstructorArgValue(workflowAggregateType)
                  .addConstructorArgReference(BEANNAME_MIGRATIONADAPERPROPERTIES)
                  .addConstructorArgValue(aggregatePersistenceAware)
                  .addConstructorArgValue(migratableProcessServices)
                  .getBeanDefinition();
              processServiceBeanDefinition.setTargetType(
                  ResolvableType.forClassWithGenerics(io.vanillabp.spi.process.ProcessService.class,
                      workflowAggregateType));
              registry.registerBeanDefinition(
                  "VanillaBP_ProcessService_%s".formatted(workflowAggregateType.getName()),
                  processServiceBeanDefinition
              );
              processServicesBuilt.add(workflowAggregateType);
            });
      } catch (Exception e) {
        throw new IllegalStateException("Could not register ProcessService beans", e);
      }

    };

  }

}