QuarkusMigrationAdapterTransformer.java

package io.vanillabp.integration.runtime.config;

import static io.vanillabp.integration.runtime.config.QuarkusMigrationAdapterProperties.PREFIX;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import io.vanillabp.integration.adapter.migration.config.AdapterProperties;
import io.vanillabp.integration.adapter.migration.config.MigrationAdapterProperties;
import io.vanillabp.integration.adapter.migration.config.WorkflowModuleAdapterProperties;
import io.vanillabp.integration.runtime.workflowmodule.WorkflowModule;
import lombok.Builder;

/**
 * Turns {@link QuarkusMigrationAdapterProperties} into
 * {@link MigrationAdapterProperties}. Validates values of properties
 * which are specific to Quarkus. Further validation is done by
 * {@link MigrationAdapterProperties#validateProperties(List, List)}
 * or {@link MigrationAdapterProperties#validatePropertiesFor(List, String, String)}.
 */
@Builder
public class QuarkusMigrationAdapterTransformer {

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

  /**
   * The properties to transform
   */
  private final QuarkusMigrationAdapterProperties properties;

  /**
   * Capabilities of Quarkus extensions available
   */
  private final Collection<String> capabilities;

  /**
   * Transforms {@link QuarkusMigrationAdapterProperties} into
   * {@link MigrationAdapterProperties}.
   *
   * @param workflowModulesFound All workflow modules found during augmentation.
   * @param adaptersFound All adapters found during augmentation.
   * @return The {@link MigrationAdapterProperties}
   * @throws IllegalStateException If validation fails
   */
  public MigrationAdapterProperties getAndValidatePropertiesConfigured(
      final Collection<WorkflowModule> workflowModulesFound,
      final Collection<String> adaptersFound) throws IllegalStateException {

    final var result = new MigrationAdapterProperties();

    result.setResourcesLocation(properties.resourcesLocation().orElse(null));

    // validate properties of adapters against adapters found in the classpath
    final var adaptersConfigured = getAndValidateAdaptersConfigured(
        adaptersFound);
    result.setAdapters(adaptersConfigured);

    // validate priorities of adapters configured against adapters found in the classpath
    final var prioritizedAdaptersConfigured = getAndValidatePrioritizedAdaptersConfigured(
        adaptersConfigured);
    result.setPrioritizedAdapters(prioritizedAdaptersConfigured);

    // validate properties of workflow modules against workflow modules found in the classpath
    final var workflowModulesConfigured = getAndValidateWorkflowModulesConfigured(
        workflowModulesFound);
    result.setWorkflowModules(workflowModulesConfigured);

    return result;

  }

  /**
   * Determine workflow module properties and validate them against workflow modules found in classpath.
   *
   * @param workflowModulesFound All workflow modules found based on META-INF/workflow-module files
   * @return Map of workflow modules (key = workflow module ID, value = properties)
   */
  private Map<String, WorkflowModuleAdapterProperties> getAndValidateWorkflowModulesConfigured(
      final Collection<WorkflowModule> workflowModulesFound) {

    final var knownWorkflowModuleIds = workflowModulesFound
        .stream()
        .map(WorkflowModule::getId)
        .toList();

    final var result = properties
        .workflowModules()
        .entrySet()
        .stream()
        .map(workflowModule -> Map.entry(
            workflowModule.getKey(),
            (WorkflowModuleAdapterProperties) WorkflowModuleAdapterProperties
                .builder()
                .workflowModuleId(workflowModule.getKey())
                .prioritizedAdapters(workflowModule.getValue().prioritizedAdapters().isPresent()
                    ? workflowModule.getValue().prioritizedAdapters().get()
                    : List.of())
                .workflows(Map.of()) // TODO fill workflows
                .adapters(workflowModule
                    .getValue()
                    .adapters()
                    .entrySet()
                    .stream()
                    .map(adapter -> Map.entry(
                        adapter.getKey(),
                        AdapterProperties
                            .builder()
                            .resourcesLocation(adapter.getValue().resourcesLocation())
                            .build()))
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))
                .build()))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    if (result.isEmpty() && !knownWorkflowModuleIds.isEmpty()) {
      final var missingConfigSections = knownWorkflowModuleIds
          .stream()
          .map(module -> "%s.workflow-modules.%s".formatted(PREFIX, module))
          .collect(Collectors.joining("', '"));
      throw new IllegalStateException(
          "No workflow-modules configured! Add properties sections '%s'.".formatted(missingConfigSections));
    }

    // check for unconfigured workflow modules
    final var unconfiguredModules = knownWorkflowModuleIds
        .stream()
        .filter(module -> !result.containsKey(module))
        .collect(Collectors.joining("\n, "));
    if (!unconfiguredModules.isEmpty()) {
      throw new IllegalStateException(
          """
              Unconfigured VanillaBP workflow modules were found in classpath:
                %s
              Add property keys '%s.workflow-modules.*' to configure them."""
              .formatted(unconfiguredModules, PREFIX));
    }

    // check for unknown adapters
    final var unknownModules = result
        .keySet()
        .stream()
        .filter(workflowModuleAdapterProperties -> !knownWorkflowModuleIds.contains(workflowModuleAdapterProperties))
        .map(workflowModuleAdapterProperties -> "%s.workflow-modules.%s".formatted(PREFIX,
            workflowModuleAdapterProperties))
        .collect(Collectors.joining("\n, "));
    if (!unknownModules.isEmpty()) {
      throw new IllegalStateException(
          """
              Property keys '%s.workflow-modules.*' must name VanillaBP workflow modules available in classpath!
              These unknown workflow modules were found in properties:
                %s
              Available workflow modules currently loaded in classpath: '%s'."""
              .formatted(PREFIX, unknownModules, String.join("', '", knownWorkflowModuleIds)));
    }

    return result;

  }

  /**
   * Determine adapters configured based on the capabilities of VanillaBP adapter
   * Quarkus extensions and properties configured.
   *
   * @param adaptersFound All adapters found during augmentation.
   * @return Map of adapters (key = adapter name, value = adapter type)
   */
  private Map<String, String> getAndValidateAdaptersConfigured(
      final Collection<String> adaptersFound) {

    // determine adapters by examining capabilities of Quarkus extensions available:
    final var adapterPackagesProvidedByOtherExtensions = capabilities
        .stream()
        .filter(capability -> capability.startsWith(PREFIX_ADAPTER_PACKAGE))
        .toList();
    final var adapterTypesProvidedByOtherExtensions = adapterPackagesProvidedByOtherExtensions
        .stream()
        .map(pkg -> pkg.substring(PREFIX_ADAPTER_PACKAGE.length()))
        .toList();
    if (adapterPackagesProvidedByOtherExtensions.isEmpty()) {
      throw new IllegalStateException(
          "No extensions found with capabilities '%s*'! Add Quarkus extensions providing VanillaBP adapters."
              .formatted(PREFIX_ADAPTER_PACKAGE));
    }

    // build result map (key = adapter id, value = adapter type)
    final var result = properties
        .adapters()
        .entrySet()
        .stream()
        // map type property, if not set, to the adapters id
        .map(config -> Map.entry(config.getKey(), config.getValue().type().orElse(config.getKey())))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    if (result.isEmpty()) {
      final var missingConfigSections = adapterTypesProvidedByOtherExtensions
          .stream()
          .map(adapter -> "%s.adapters.xxxx.type=%s".formatted(PREFIX, adapter))
          .collect(Collectors.joining("\n  "));
      throw new IllegalStateException(
          """
              No adapters configured! Add properties sections for your BPMS (e.g. xxx) having type set to adapters found in classpath:
                %s"""
              .formatted(missingConfigSections));
    }

    // check for unknown adapters
    final var unknownAdapters = result
        .entrySet()
        .stream()
        .filter(adapter -> !adapterTypesProvidedByOtherExtensions.contains(adapter.getValue()))
        .map(adapter -> "'%s' found in '%s.adapters.%s.type'".formatted(adapter.getValue(), PREFIX, adapter.getKey()))
        .collect(Collectors.joining("\n  "));
    if (!unknownAdapters.isEmpty()) {
      throw new IllegalStateException(
          """
              Properties '%s.adapters.*.type' must contain VanillaBP adapters added as Quarkus extension!
              These adapters are unknown:
                %s
              Available adapter types provided by Quarkus extensions currently loaded: %s."""
              .formatted(PREFIX, unknownAdapters,
                  String.join(", ", adapterTypesProvidedByOtherExtensions)));
    }

    // validate adapters provided by VanillaBP Quarkus adapter extensions
    final var extensionsWithoutCapability = adaptersFound
        .stream()
        .filter(adapter -> !adapterTypesProvidedByOtherExtensions.contains(adapter))
        .collect(Collectors.joining("\n  "));
    if (!extensionsWithoutCapability.isEmpty()) {
      throw new IllegalStateException(
          """
              Illegal VanillaBP adapter extensions:
                '%s'
              are not matching their extension capabilities!"""
              .formatted(extensionsWithoutCapability));
    }

    // validate properties against adapters provided by VanillaBP Quarkus adapter extensions
    final var missingAdapters = result
        .values()
        .stream()
        .filter(type -> !adaptersFound.contains(type))
        .collect(Collectors.joining("\n  "));
    if (!missingAdapters.isEmpty()) {
      throw new IllegalStateException(
          """
              Missing VanillaBP adapter extensions for these types found in properties:
                %s"""
              .formatted(missingAdapters));
    }

    // validate properties of process services against adapters provided by VanillaBP Quarkus adapter extensions
    final var buildItemsNotConfigured = adaptersFound
        .stream()
        .filter(Predicate.not(result::containsValue))
        .map(adapter -> "%s.adapters.*.type=%s".formatted(PREFIX, adapter))
        .collect(Collectors.joining("\n  "));
    if (!buildItemsNotConfigured.isEmpty()) {
      throw new IllegalStateException(
          """
              VanillaBP Quarkus adapter extensions found but not configured.
              Add adapter specific configuration in properties sections having type set:
                %s"""
              .formatted(buildItemsNotConfigured));
    }

    // test for adapters not found in properties
    final var unconfiguredAdapters = adapterTypesProvidedByOtherExtensions
        .stream()
        .filter(adapter -> !result.containsValue(adapter))
        .collect(Collectors.joining(", "));
    if (!unconfiguredAdapters.isEmpty()) {
      throw new IllegalStateException(
          """
              No '%s.adapters.*' properties sections having types provided by Quarkus extension!
              Add section section if intended or remove extensions for these types: %s."""
              .formatted(PREFIX, unconfiguredAdapters));
    }

    return result;

  }

  /**
   * Determine priorities of adapters configured.
   *
   * @param adapters The adapters found
   * @return List of adapter ordered by configured priorities
   */
  private List<String> getAndValidatePrioritizedAdaptersConfigured(
      final Map<String, String> adapters) {

    // It is OK to skip property vanillabp.prioritized-adapters in case
    // only one adapter is configured:
    if (properties.prioritizedAdapters().isEmpty() && (adapters.size() == 1)) {
      return adapters.keySet().stream().toList();
    }

    // if more than one adapter is configured, then the
    // property vanillabp.prioritized-adapters has to list each adapter
    // configured:
    if (properties.prioritizedAdapters()
        .isEmpty() || (adapters.size() != properties.prioritizedAdapters().get().size())) {
      throw new IllegalStateException(
          """
              The property '%s.prioritized-adapters' must list all the adapters configured in '%s.adapters.*' to define
              the order in which adapters are addressed to find workflows running.
              Configured adapters are: %s."""
              .formatted(PREFIX, PREFIX, String.join(", ", adapters.keySet())));
    }

    final var unknownAdapters = properties
        .prioritizedAdapters()
        .get()
        .stream()
        .filter(adapter -> !adapters.containsKey(adapter))
        .map(adapter -> "%s -> '%s.adapters.%s'".formatted(adapter, PREFIX, adapter))
        .collect(Collectors.joining("\n  "));
    if (!unknownAdapters.isEmpty()) {
      throw new IllegalStateException(
          """
              The property '%s.prioritized-adapters' lists these adapters for which no property sections were found:
                %s"""
              .formatted(PREFIX, unknownAdapters));
    }

    return properties.prioritizedAdapters().get();

  }

}