WorkflowModuleAutoConfiguration.java
package io.vanillabp.integration.workflowmodule;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.lang.Nullable;
import io.vanillabp.spi.service.WorkflowService;
import lombok.extern.slf4j.Slf4j;
/**
* Autoconfiguration of VanillaBP workflow modules.
*/
@Slf4j
@Configuration
public class WorkflowModuleAutoConfiguration {
/**
* Build a bean holding all workflow modules found.
*
* @param resourceLoader The resource loader used to find META-INF/workflow-module files
* @return The workflow modules found
*/
@Bean
public static WorkflowModules vanillaBpWorkflowModules(
final ResourceLoader resourceLoader) {
return determineWorkflowModules(resourceLoader);
}
/**
* Searches for all workflow module descriptors found in classpath to build
* {@link WorkflowModule} objects. This method is static because it has to be processed
* by Spring Boot during the very beginning of booting the application.
* The bean returned is used for loading workflow module-specific config files.
*
* @param resourceLoader The resource loader used to find META-INF/workflow-module files
* @return The workflow modules found
*/
static WorkflowModules determineWorkflowModules(
@Nullable final ResourceLoader resourceLoader) {
try {
final var workflowModuleDescriptors = ResourcePatternUtils
.getResourcePatternResolver(resourceLoader)
.getResources("classpath*:%s".formatted(WorkflowModule.METAINF_WORKFLOWMODULE));
final var workflowModules = Arrays
.stream(workflowModuleDescriptors)
.map(resource -> {
try {
final var workflowModuleId = resource
.getContentAsString(Charset.defaultCharset())
.trim();
if (workflowModuleId.isEmpty()) {
throw new IllegalStateException(
"Empty workflow module descriptor '"
+ resource.getURI()
+ "'");
}
return new WorkflowModule(workflowModuleId, resource.getURI());
} catch (IOException e) {
throw new BeanCreationException(
"Could not load workflow module descriptors from classpath '"
+ WorkflowModule.METAINF_WORKFLOWMODULE
+ "'");
}
})
.toList();
return new WorkflowModules(workflowModules);
} catch (IOException e) {
throw new BeanCreationException(
"Could not load workflow module descriptors from classpath '"
+ WorkflowModule.METAINF_WORKFLOWMODULE
+ "'");
}
}
/**
* Associates workflow services with workflow modules for later usage.
*
* @param allWorkflowModules All workflow modules found in the classpath
* @param allWorkflowServiceClasses All classes of workflow services found
*/
public static void registerProcessServices(
final List<WorkflowModule> allWorkflowModules,
final List<Class<?>> allWorkflowServiceClasses) {
final var globalWorkflowModuleWorkflowServiceClasses = new LinkedList<Class<?>>();
final var globalClasspathWorkflowModuleDescriptors = new LinkedList<>(allWorkflowModules);
// apply all service classes to their workflow modules
allWorkflowServiceClasses
.forEach(serviceClass -> {
try {
// register a service class in the workflow module identified by META-INF/workflow-service
// found in the same JAR/directory
final var serviceClassSourceUri = serviceClass
.getProtectionDomain()
.getCodeSource()
.getLocation()
.toURI();
final URI serviceClassWorkflowDescriptorUrl;
if (serviceClassSourceUri.getPath().endsWith(".jar")) {
serviceClassWorkflowDescriptorUrl = URI
.create(
"jar:%s!/%s".formatted(serviceClassSourceUri.toString(), WorkflowModule.METAINF_WORKFLOWMODULE));
} else {
serviceClassWorkflowDescriptorUrl = serviceClassSourceUri
.resolve(WorkflowModule.METAINF_WORKFLOWMODULE);
}
final var workflowModuleInServiceClassJar = allWorkflowModules
.stream()
.filter(module -> module.getSourceUri().equals(serviceClassWorkflowDescriptorUrl))
.findFirst();
if (workflowModuleInServiceClassJar.isPresent()) {
globalClasspathWorkflowModuleDescriptors.remove(workflowModuleInServiceClassJar.get());
workflowModuleInServiceClassJar.get().addWorkflowService(serviceClass);
return;
}
// load workflow module ID from META-INF/workflow-module of the Java module JAR
// the workflow service class belongs to
// TODO: NOT YET SUPPORTED
// collect service class for later registration in global workflow module
globalWorkflowModuleWorkflowServiceClasses.add(serviceClass);
} catch (Exception e) {
throw new IllegalStateException(
"Could not to determine workflow module id", e);
}
});
if (globalClasspathWorkflowModuleDescriptors.size() > 1) {
throw new IllegalStateException("""
Multiple workflow module descriptor files %s were found in modules which do not contain any service class annotated with @%s:
- %s
"""
.formatted(
WorkflowModule.METAINF_WORKFLOWMODULE,
WorkflowService.class.getName(),
globalClasspathWorkflowModuleDescriptors
.stream()
.map(WorkflowModule::getSourceUri)
.map(URI::toString)
.collect(Collectors.joining("\n - "))));
}
// no global workflow module descriptor file
if (globalClasspathWorkflowModuleDescriptors.isEmpty()) {
// if no workflow services left, then it is OK
if (globalWorkflowModuleWorkflowServiceClasses.isEmpty()) {
return;
}
throw new IllegalStateException("""
There is no workflow module descriptor file %s in the application's module nor, if in a separate module, in the modules of these workflow service classes:
- %s
"""
.formatted(
WorkflowModule.METAINF_WORKFLOWMODULE,
globalWorkflowModuleWorkflowServiceClasses
.stream()
.map(Class::getName)
.collect(Collectors.joining("\n - "))
));
}
// associate workflow services to global workflow module if not yet associated to another workflow module
final var globalClasspathWorkflowModuleDescriptor = globalClasspathWorkflowModuleDescriptors
.getFirst();
globalClasspathWorkflowModuleDescriptor.addWorkflowServices(globalWorkflowModuleWorkflowServiceClasses);
}
}