SpringBootDeploymentService.java

package io.vanillabp.integration.deployment;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import io.vanillabp.integration.adapter.migration.deployment.DeploymentService;
import io.vanillabp.integration.workflowmodule.WorkflowModule;
import io.vanillabp.integration.workflowmodule.WorkflowModules;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;

/**
 * Manages deployment of resources using {@link DeploymentService}.
 */
@RequiredArgsConstructor
public class SpringBootDeploymentService {

  private final DeploymentService deploymentService;

  private final WorkflowModules allWorkflowModules;

  /**
   * Triggers loading of all BPMN resources and deployment of them.
   */
  @PostConstruct
  public void deployResources() {

    deploymentService.deployResources(
        getWorkflowModuleIds(),
        this::bpmnResourcesLoader);

  }

  /**
   * Start processing of workflows one the application started.
   */
  @EventListener(ApplicationReadyEvent.class)
  public void startProcessingOfWorkflows() {

    deploymentService.startWorkflowProcessing(
        getWorkflowModuleIds());

  }

  private List<String> getWorkflowModuleIds() {

    return allWorkflowModules
        .getWorkflowModules()
        .stream()
        .map(WorkflowModule::getId)
        .toList();

  }

  /**
   * Recursively reads all BPMN resources from the given location.
   *
   * @param resourceLocation The location to read from
   * @return A map of relative paths to BPMN resources
   */
  public Map<String, InputStream> bpmnResourcesLoader(
      final String resourceLocation) {

    final var resolver = new PathMatchingResourcePatternResolver();

    final var normalizedLocation = resourceLocation.endsWith("/")
        ? resourceLocation
        : resourceLocation
            + "/";

    final var resourcePattern = normalizedLocation
        + "**/*.bpmn";

    final Resource[] resources;
    try {
      resources = resolver.getResources(resourcePattern);
    } catch (final IOException e) {
      throw new IllegalStateException(
          "Failed to resolve BPMN resources from location: "
              + resourceLocation, e
      );
    }

    final var result = new HashMap<String, InputStream>();

    for (final var resource : resources) {
      if (!resource.isReadable()) {
        continue;
      }

      final URI uri;
      try {
        uri = resource.getURI();
      } catch (final IOException e) {
        throw new IllegalStateException("Failed to resolve URI for resource", e);
      }

      final String relativePath = extractRelativePath(uri, normalizedLocation);
      if (relativePath == null) {
        continue;
      }

      try {
        result.put(relativePath, resource.getInputStream());
      } catch (final IOException e) {
        throw new IllegalStateException(
            "Failed to open InputStream for resource: "
                + relativePath, e
        );
      }
    }

    return result;

  }

  private String extractRelativePath(
      final URI uri,
      final String resourceLocation) {

    final var uriString = uri.toString();

    /*
     * Examples:
     * jar:file:/app.jar!/BOOT-INF/classes/bpmn/order/process.bpmn
     * file:/opt/app/bpmn/order/process.bpmn
     * classpath:/bpmn/order/process.bpmn
     */

    final var index = uriString.indexOf(resourceLocation.replace("classpath*:", "")
        .replace("classpath:", "")
        .replace("file:", ""));
    if (index < 0) {
      return null;
    }

    final var relative = uriString.substring(index)
        .replaceFirst("^.*/", "")
        .replace("\\", "/");

    return relative;

  }

}