DeploymentService.java

package io.vanillabp.integration.adapter.migration.deployment;

import java.io.InputStream;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

import io.vanillabp.integration.adapter.migration.config.MigrationAdapterProperties;
import io.vanillabp.intergration.adapter.spi.AdapterDeploymentService;
import io.vanillabp.intergration.extension.spi.ExtensionWiringService;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

/**
 * A service responsible for deploying BPMS resources like BPMN and DMN files.
 */
@Slf4j
public class DeploymentService {

  /**
   * @see DeploymentService#bpmsProcessingContexts
   * @param <PC> The processing context, used to store all information needed by the adapter to deploy the process.
   */
  @Builder
  @Getter
  private static class ToBeStarted<PC> {
    private AdapterDeploymentService<?, ?, PC> deploymentService;
    private PC bpmsProcessingContext;
  }

  @Getter
  @Setter
  private static class BpmsProcessingContextHolder<PC> {
    private PC bpmsProcessingContext;

    public boolean isEmpty() {
      return bpmsProcessingContext == null;
    }
  }

  /**
   * VanillaBP properties.
   */
  private final MigrationAdapterProperties properties;

  /**
   * Deployment services of all adapters.
   */
  private final List<AdapterDeploymentService<?, ?, ?>> deploymentServices;

  /**
   * Wiring services of all adapters and extensions.
   */
  private final List<ExtensionWiringService<?, ?>> wiringServices;

  /**
   * A map of workflow module ids and a list of process-contexts to be started after deployment and booting the application.
   */
  private final Map<String, ToBeStarted<?>> bpmsProcessingContexts;

  /**
   * @param properties Attributes for configuration of the deployment process.
   * @param deploymentServices All adapters deployment services.
   */
  public DeploymentService(
      final MigrationAdapterProperties properties,
      final List<AdapterDeploymentService<?, ?, ?>> deploymentServices,
      final List<ExtensionWiringService<?, ?>> wiringServices) {

    this.properties = properties;
    this.deploymentServices = deploymentServices;
    this.wiringServices = new LinkedList<>(wiringServices);
    this.wiringServices.sort(Comparator.comparingInt(ExtensionWiringService::getOrder));
    bpmsProcessingContexts = new HashMap<>();

  }

  /**
   * Deploy all resources of all workflow modules to the BPMS. This is done
   * <ol>
   *   <li>for each adapter configured for the workflow-module (prioritized adapters)</li>
   *   <li>reading the BPMN/DMN files</li>
   *   <li>prepare the BPMN by the adapter (setting default behavior etc.)</li>
   *   <li>for each adapter and extension having the same model type and process context type (e.g. all for Camunda 8)</li>
   *   <ol>
   *     <li>wire the business code to the BPMN's tasks</li>
   *   </ol>
   *   <li>deploy the result to the BPMS</li>
   * </ol>
   *
   * @param workflowModuleIds The workflow module IDs to deploy
   * @param bpmnResourcesLoader A function that takes a resource location and loads the BPMN resources
   *     for a given workflow module ID. It provides a
   *     map having the filename as the key and the BPMN input stream as the value.
   * @param <PC> The processing context, used to store all information needed by the adapter to deploy the process.
   */
  @SuppressWarnings("unchecked")
  public <PC> void deployResources(
      final List<String> workflowModuleIds,
      final Function<String, Map<String, InputStream>> bpmnResourcesLoader) {

    // walk through all workflow modules
    workflowModuleIds
        .stream()
        // for each adapter configured...
        .flatMap(workflowModuleId -> properties
            .getPrioritizedAdaptersFor(workflowModuleId)
            .stream()
            // ...find the right deployment service...
            .map(adapterId -> deploymentServices
                .stream()
                .filter(service -> service.getAdapterId().equals(adapterId))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException(
                    "No deployment service found for adapter '%s'!".formatted(adapterId)
                )))
            .map(deploymentService -> Map.entry(
                workflowModuleId,
                (AdapterDeploymentService<?, ?, PC>) deploymentService)))
        // and process the resources of the workflow module specific to the adapter
        .forEach(deploymentServiceEntry -> {
          final var workflowModuleId = deploymentServiceEntry.getKey();
          final var deploymentService = deploymentServiceEntry.getValue();
          final var resourceLocationEntry = properties.getAdapterResourcesLocationFor(
              workflowModuleId,
              deploymentService.getAdapterId());
          final var isVanillaBpBpmn = resourceLocationEntry.getValue();
          final var resourceLocation = resourceLocationEntry.getKey();
          // find all BPMN files in the resource location...
          final var bpmsProcessingContext = new BpmsProcessingContextHolder<PC>();
          bpmnResourcesLoader
              .apply(resourceLocation)
              .entrySet()
              // ...and process them...
              .forEach(bpmnFileEntry -> processBpmn(
                  workflowModuleId,
                  deploymentService,
                  bpmsProcessingContext.getBpmsProcessingContext(),
                  bpmnFileEntry.getKey(), // filename
                  bpmnFileEntry.getValue(), // InputStream
                  isVanillaBpBpmn)
                  .ifPresentOrElse(
                      bpmsProcessingContext::setBpmsProcessingContext,
                      () -> log.warn(
                          "File '{}‘ of workflow module '{}' did not contain any executable processes. Skipping deployment of this file!",
                          bpmnFileEntry.getKey(),
                          workflowModuleId)));
          // ...and finally deploy all the resources together (BPMN, DMN) to the BPMS
          deploymentService.deployResources(workflowModuleId, bpmsProcessingContext.getBpmsProcessingContext());
          bpmsProcessingContexts.put(workflowModuleId, ToBeStarted
              .<PC>builder()
              .deploymentService(deploymentService)
              .bpmsProcessingContext(bpmsProcessingContext.getBpmsProcessingContext())
              .build());
        });

  }

  /**
   * Process the given BPMN file.
   *
   * @param workflowModuleId The workflow module ID
   * @param deploymentService The deployment service to use
   * @param bpmsProcessingContext The context used to store all information needed by the adapter to deploy the process
   * @param filename The filename of the BPMN file (used for logging and error messages)
   * @param bpmn The BPMN resource inputstream
   * @param isVanillaBpBpmn Whether the BPMN is VanillaBP's BPMN or is specific to the adapter's BPMS
   * @param <BPMN> The BPMN model type
   * @param <DMN> The DMN model type
   * @param <PC> The processing context, used to store all information needed by the adapter to deploy the process
   * @return The context used to store all information needed by the adapter to deploy the process
   */
  @SuppressWarnings("unchecked")
  private <BPMN, DMN, PC> Optional<PC> processBpmn(
      final String workflowModuleId,
      final AdapterDeploymentService<BPMN, DMN, PC> deploymentService,
      final PC bpmsProcessingContext,
      final String filename,
      final InputStream bpmn,
      final boolean isVanillaBpBpmn) {

    return deploymentService
        // read executable processes from the BPMN file
        .readBpmn(
            workflowModuleId,
            filename,
            bpmn,
            isVanillaBpBpmn)
        .stream()
        // process each executable process by...
        .map(processIdAndModel -> {
          final var bpmnProcessId = processIdAndModel.getKey();
          final var bpmnModel = processIdAndModel.getValue();
          // ...preparing the model...
          final var context = deploymentService.prepareBpmn(
              workflowModuleId,
              bpmsProcessingContext,
              filename,
              bpmnProcessId,
              bpmnModel);
          // ...wire the business code to the BPMN's tasks...
          deploymentService.wireBpmn(
              workflowModuleId,
              filename,
              bpmnProcessId,
              bpmnModel,
              context);
          // ...and do wiring for all matching extensions wiring services found
          wiringServices
              .stream()
              .filter(wiringService -> wiringService.getModelType().equals(bpmnModel.getClass()))
              .filter(wiringService -> wiringService.getProcessContextType().equals(context.getClass()))
              .map(wiringService -> (ExtensionWiringService<BPMN, PC>) wiringService)
              .forEach(wiringService -> wiringService.wireBpmn(
                  workflowModuleId,
                  filename,
                  bpmnProcessId,
                  bpmnModel,
                  bpmsProcessingContext));
          return context;
        })
        .findFirst();

  }

  /**
   * Starts to running the workflows of the given BPMN processes.
   *
   * @param workflowModuleIds The workflow module IDs to deploy
   * @param <PC> The processing context, used to store all information needed by the adapter to deploy the process.
   */
  @SuppressWarnings("unchecked")
  public <BPMN, PC> void startWorkflowProcessing(
      final List<String> workflowModuleIds) {

    // walk through all workflow modules...
    workflowModuleIds
        .forEach(workflowModuleId -> {
          final var bpmsProcessingContext = this.bpmsProcessingContexts.get(workflowModuleId);
          if (bpmsProcessingContext == null) {
            return;
          }
          final var deploymentService = (AdapterDeploymentService<?, ?, PC>) bpmsProcessingContext.deploymentService;
          final var processingContext = (PC) bpmsProcessingContext.getBpmsProcessingContext();
          // ...and start workflow processing for each adapter
          deploymentService
              .startWorkflowProcessing(
                  workflowModuleId,
                  processingContext);
        });

    // walk through all workflow modules...
    workflowModuleIds
        .forEach(workflowModuleId -> {
          final var bpmsProcessingContext = this.bpmsProcessingContexts.get(workflowModuleId);
          if (bpmsProcessingContext == null) {
            return;
          }
          final var deploymentService = (AdapterDeploymentService<?, ?, PC>) bpmsProcessingContext.deploymentService;
          final var processingContext = (PC) bpmsProcessingContext.getBpmsProcessingContext();
          // ...and start workflow processing for each extension
          wiringServices
              .stream()
              .filter(wiringService -> wiringService.getModelType().equals(deploymentService.getModelType()))
              .filter(wiringService -> wiringService.getProcessContextType()
                  .equals(deploymentService.getProcessContextType()))
              .map(wiringService -> (ExtensionWiringService<BPMN, PC>) wiringService)
              .forEach(wiringService -> wiringService.startWorkflowProcessing(
                  workflowModuleId,
                  processingContext));
        });

  }

}