WorkflowModuleBuildStepProcessor.java
package io.vanillabp.integration.deployment.workflowmodule;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.jandex.ClassInfo;
import io.quarkus.deployment.GeneratedClassGizmoAdaptor;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;
import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem;
import io.quarkus.deployment.builditem.StaticInitConfigBuilderBuildItem;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.runtime.configuration.ConfigBuilder;
import io.vanillabp.integration.runtime.config.WorkflowModuleSpecificPropertiesConfigBuilder;
import io.vanillabp.integration.runtime.config.WorkflowModuleSpecificPropertiesConfigSourceProvider;
import io.vanillabp.integration.runtime.config.WorkflowModuleSpecificYamlConfigBuilder;
import io.vanillabp.integration.runtime.config.WorkflowModuleSpecificYamlConfigSourceProvider;
import io.vanillabp.integration.runtime.workflowmodule.WorkflowModule;
import lombok.extern.slf4j.Slf4j;
/**
* VanillaBP extension build step processor, responsible for processing workflow modules.
*/
@Slf4j
public class WorkflowModuleBuildStepProcessor {
/**
* Priority of workflow-module-specific YAML config files. A tick more important than the original "application.yaml".
*
* @see WorkflowModuleBuildStepProcessor#buildWorkflowModuleSpecificConfigFilesConfigBuilder(List, VanillaBpWorkflowModulesBuildItem, BuildProducer)
*/
public static final int YAML_CONFIGFILE_ORDINAL = 256;
/**
* Priority of workflow-module-specific Properties config files. A tick more important than the original "application.properties".
*
* @see WorkflowModuleBuildStepProcessor#buildWorkflowModuleSpecificConfigFilesConfigBuilder(List, VanillaBpWorkflowModulesBuildItem, BuildProducer)
*/
public static final int PROPERTIES_CONFIGFILE_ORDINAL = 251;
/**
* Cache to accelerate the augmentation phase.
*/
private static final Map<ClassInfo, WorkflowModule> resolvedWorkflowModuleIds = new HashMap<>();
/**
* Finds all workflow modules specified by <code>META-INF/workflow-module</code> files in their
* Maven/Gradle module.
*
* @param applicationArchivesBuildItem The archives part of this Quarkus build
* @return A build item holding meta-information of all workflow modules
*/
@BuildStep
VanillaBpWorkflowModulesBuildItem findAllWorkflowModules(
final ApplicationArchivesBuildItem applicationArchivesBuildItem) {
final var workflowModules = applicationArchivesBuildItem
// search all archives of the project
.getAllApplicationArchives()
.stream()
.flatMap(archive -> Optional
// for META-INF/workflow-module files
.ofNullable(archive.getChildPath(WorkflowModule.METAINF_WORKFLOWMODULE))
.map(Path::toUri)
.flatMap(sourceUri -> WorkflowModuleBuildStepProcessor
// and read them
.readWorkflowModuleDescriptor(sourceUri)
// to build a WorkflowModule object
.map(id -> Map.entry(WorkflowModule
.builder()
.id(id)
.sourceUri(sourceUri)
.global(archive.equals(applicationArchivesBuildItem.getRootArchive()))
.build(), archive)))
.stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
return VanillaBpWorkflowModulesBuildItem
.builder()
.workflowModules(workflowModules)
.build();
}
/**
* Builds {@link ConfigBuilder} classes responsible for loading properties from files specific to a workflow
* module by using the workflow module's ID as a filename instead of <code>application.properties</code>.
* Those files support modularization of workflow modules because next to code and BPMS resources (e.g.
* BPMN files) also configuration can be placed in the same Maven/Gradle module.
*
* <table>
* <caption>Default ConfigSource ordinals (Quarkus / SmallRye) and ordinals of workflow module config sources</caption>
* <thead>
* <tr>
* <th>Config Source</th>
* <th style="min-width:120px;">Ordinal</th>
* <th>Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td>System properties (e.g. <code>-Dfoo=bar</code>)</td>
* <td><nobr>400</nobr></td>
* <td>Highest priority (MicroProfile / SmallRye standard)</td>
* </tr>
* <tr>
* <td>Environment variables</td>
* <td><nobr>300</nobr></td>
* <td>Typical OS / container environment variables</td>
* </tr>
* <tr>
* <td><code>.env</code> file in the current working directory</td>
* <td><nobr>295</nobr></td>
* <td>Optional, if present Quarkus loads it automatically</td>
* </tr>
* <tr>
* <td><code>$PWD/config/application.yaml</code> (config directory, if <code>quarkus-config-yaml</code> is enabled)</td>
* <td><nobr>265</nobr></td>
* <td>Filesystem YAML has higher priority than classpath YAML</td>
* </tr>
* <tr>
* <td><code>$PWD/config/application.properties</code> (config directory next to runner)</td>
* <td><nobr>260</nobr></td>
* <td>External configuration file outside of the JAR</td>
* </tr>
* <tr>
* <td><b><code>XXX.yaml</code> (classpath, if <code>quarkus-config-yaml</code> is enabled)</b></td>
* <td><nobr><b>256</b></nobr></td>
* <td><b>YAML on the classpath specific to a certain workflow module having ID "XXX".
* Lower priority than filesystem <i>application.yaml</i> since external files may be used to
* override values without the need for rebuilding and deploying the application.</b></td>
* </tr>
* <tr>
* <td><code>application.yaml</code> (classpath, if <code>quarkus-config-yaml</code> is enabled)</td>
* <td><nobr>255</nobr></td>
* <td>YAML on classpath; Quarkus gives YAML higher priority than classpath properties</td>
* </tr>
* <tr>
* <td><b><code>XXX.properties</code> (classpath, e.g. <code>src/main/resources</code>)</b></td>
* <td><nobr><b>251</b></nobr></td>
* <td><b>Properties on the classpath specific to a certain workflow module having ID "XXX".
* Lower priority than filesystem <i>application.properties</i> since external files may be used to
* override values without the need for rebuilding and deploying the application.</b></td>
* </tr>
* <tr>
* <td><code>application.properties</code> (classpath, e.g. <code>src/main/resources</code>)</td>
* <td><nobr>250</nobr></td>
* <td>Default classpath application.properties</td>
* </tr>
* <tr>
* <td><code>META-INF/microprofile-config.properties</code> (classpath)</td>
* <td><nobr>100</nobr></td>
* <td>MicroProfile standard source (lowest practical priority)</td>
* </tr>
* </tbody>
* </table>
*
* @param features All features provided by extensions
* @param workflowModules All workflow modules found
* @param generatedClassesProducer Producer to build new classes
* @return The build item holding the names of all {@link ConfigBuilder} classes
*/
@BuildStep
GeneratedConfigBuilderClassesBuildItem buildWorkflowModuleSpecificConfigFilesConfigBuilder(
final List<FeatureBuildItem> features,
final VanillaBpWorkflowModulesBuildItem workflowModules,
final BuildProducer<GeneratedClassBuildItem> generatedClassesProducer) {
final var configBuilderClassnames = new LinkedList<String>();
// add xxx.properties files
configBuilderClassnames.add(
// get ConfigBuilder class generated to load files for all workflow modules
addWorkflowModuleSpecificConfigBuilder(
PROPERTIES_CONFIGFILE_ORDINAL,
WorkflowModuleSpecificPropertiesConfigBuilder.class,
workflowModules,
generatedClassesProducer));
// add xxx.yaml files
final var yamlConfigExtensionAvailable = features
.stream()
.anyMatch(feature -> feature.getName().equals("config-yaml"));
if (yamlConfigExtensionAvailable) {
configBuilderClassnames.add(
// get ConfigBuilder class generated to load files for all workflow modules
addWorkflowModuleSpecificConfigBuilder(
YAML_CONFIGFILE_ORDINAL,
WorkflowModuleSpecificYamlConfigBuilder.class,
workflowModules,
generatedClassesProducer));
}
return GeneratedConfigBuilderClassesBuildItem
.builder()
.configBuilderClassnames(configBuilderClassnames)
.build();
}
/**
* Activate {@link ConfigBuilder} classes generated for workflow module specific files. This is
* only done for custom workflow module properties because VanillaBP properties are needed already
* during augmentation (see
* {@link io.vanillabp.integration.deployment.config.QuarkusMigrationAdapterPropertiesConfigBuilderCustomizer}).
* <p>
* This is done in a separate step to ensure generated classes are available. If this had been done in
* one build step, then there would have been no guarantees that the classes are available
* when building the configuration. based on the {@link ConfigBuilder}.
*
* @param generatedConfigLoaders The build item holding the names of all {@link ConfigBuilder} classes
* @param staticInitConfigProducer Producer for static initialization config builders
* @param runTimeConfigProducer Producer for static runtime config builders
*/
@BuildStep
WorkflowModuleSpecificConfigBuilderBuildItem addWorkflowModuleSpecificConfigFiles(
final GeneratedConfigBuilderClassesBuildItem generatedConfigLoaders,
final BuildProducer<StaticInitConfigBuilderBuildItem> staticInitConfigProducer,
final BuildProducer<RunTimeConfigBuilderBuildItem> runTimeConfigProducer) {
generatedConfigLoaders
.getConfigBuilderClassnames()
.forEach(className -> {
staticInitConfigProducer.produce(new StaticInitConfigBuilderBuildItem(className));
runTimeConfigProducer.produce(new RunTimeConfigBuilderBuildItem(className));
});
return new WorkflowModuleSpecificConfigBuilderBuildItem();
}
/**
* Adds a {@link ConfigBuilder} class holding the workflow module IDs used to load files
* next to the ordinal used to prioritize the builder next to other builders.
* <pre>
* package of.abstract.config.builder.class;
* import java.util.List;
* public class ActualNameOfAbstractConfigBuilderClass extends NameOfAbstractConfigBuilderClass {
* protected int getOrdinal() {
* return 4711;
* }
* protected List<String> getWorkflowModuleIds() {
* return List.of("id1", "id2", ...);
* }
* }
* </pre>
*
* @param ordinal The ordinal to prioritize
* @param abstractConfigBuilderClass The abstract class used as a super class for the {@link ConfigBuilder} class generated
* @param workflowModules All workflow modules found
* @param generatedClassesProducer Producer for new classes
* @return The name of the class generated
*/
private String addWorkflowModuleSpecificConfigBuilder(
final int ordinal,
final Class<? extends ConfigBuilder> abstractConfigBuilderClass,
final VanillaBpWorkflowModulesBuildItem workflowModules,
final BuildProducer<GeneratedClassBuildItem> generatedClassesProducer) {
// the classname
final var configBuilderClassname = "%s.Actual%s".formatted(
abstractConfigBuilderClass.getPackageName(),
abstractConfigBuilderClass.getSimpleName());
// the class
try (final var configBuilderClassCreator = ClassCreator
.builder()
.className(configBuilderClassname)
.superClass(abstractConfigBuilderClass)
/*
.classOutput((
className,
data) -> {
if (log.isDebugEnabled()) {
log.debug("=== Gizmo generated: {} ===", className);
final var reader = new ClassReader(data);
final var writer = new StringWriter();
final var traceClassVisitor = new TraceClassVisitor(new PrintWriter(writer));
reader.accept(traceClassVisitor, 0);
log.debug(writer.toString());
log.debug("=== END {} ===", className);
}
})
*/
.classOutput(new GeneratedClassGizmoAdaptor(generatedClassesProducer, true))
.build()) {
// the method "getWorkflowModuleIds"
try (final var methodCreator = configBuilderClassCreator
.getMethodCreator("getWorkflowModuleIds", List.class)
.setModifiers(Modifier.PROTECTED)) {
final var workflowModuleIds = workflowModules
.getWorkflowModules()
.keySet()
.stream()
.map(WorkflowModule::getId)
.toList();
final var array = methodCreator.newArray(String.class, workflowModuleIds.size());
for (int i = 0; i < workflowModuleIds.size(); i++) {
methodCreator.writeArrayValue(array, i, methodCreator.load(workflowModuleIds.get(i)));
}
final var list = methodCreator.invokeStaticMethod(
MethodDescriptor.ofMethod(Arrays.class, "asList", List.class, Object[].class),
array);
methodCreator.returnValue(list);
}
// the method "getOrdinal"
try (final var methodCreator = configBuilderClassCreator
.getMethodCreator("getOrdinal", int.class)
.setModifiers(Modifier.PROTECTED)) {
methodCreator.returnInt(ordinal);
}
}
return configBuilderClassname;
}
/**
* Watches module-specific configuration files for hot deployment in a workflow application.
* This method identifies and monitors all possible configuration files (properties or YAML)
* associated with workflow modules as defined in the build process, scanning them from the
* application archives.
*
* @param allWorkflowModules A {@link VanillaBpWorkflowModulesBuildItem} containing information about all workflow modules.
* @param applicationArchives An {@link ApplicationArchivesBuildItem} containing the archives to be scanned for configuration files.
* @param watchedFiles A {@link BuildProducer} that collects {@link HotDeploymentWatchedFileBuildItem} instances for hot deployment purposes.
*/
@BuildStep
void watchWorkflowModuleSpecificConfigFiles(
final VanillaBpWorkflowModulesBuildItem allWorkflowModules,
final ApplicationArchivesBuildItem applicationArchives,
final BuildProducer<HotDeploymentWatchedFileBuildItem> watchedFiles) {
// determine all filenames possible
final var propertiesFileExtensions = new WorkflowModuleSpecificPropertiesConfigSourceProvider(
"any", -1)
.getFileExtensions();
final var yamlFileExtensions = new WorkflowModuleSpecificYamlConfigSourceProvider(
"any", -1)
.getFileExtensions();
final var extensionPattern = Stream
// combine each file extension possible for later use as or-expression
.concat(
Arrays.stream(propertiesFileExtensions),
Arrays.stream(yamlFileExtensions))
.collect(Collectors.joining("|"));
final var filenamePatterns = allWorkflowModules
.getWorkflowModules()
.keySet()
.stream()
.map(WorkflowModule::getId)
// to build a regex pattern "id*.(?:extension1|extension2)"
.map(id -> "%s.*\\.(?:%s)".formatted(id, extensionPattern))
.map(Pattern::compile)
.toList();
// search archives for config files
applicationArchives
.getAllApplicationArchives()
// traverse all archives
.forEach(archive -> archive
.accept(openPathTree -> openPathTree
.walk(visit -> filenamePatterns.
// and check each file to match one of the patterns
forEach(pattern -> Optional
.ofNullable(visit.getPath())
.map(Path::getFileName)
.map(Path::toString)
.filter(filename -> pattern.matcher(filename).matches())
.map(HotDeploymentWatchedFileBuildItem::new)
.ifPresent(watchedFiles::produce)))));
}
/**
* Determines the workflow module ID for a given {@link io.vanillabp.spi.process.ProcessService} class.
*
* @param workflowModulesFound Information about all workflow modules found in the project
* @param applicationArchivesBuildItem Information about All archives (JARs and directories) of the project
* @param serviceClass The {@link io.vanillabp.spi.process.ProcessService} class
* @return The workflow module ID
*/
public static String getWorkflowModuleId(
final VanillaBpWorkflowModulesBuildItem workflowModulesFound,
final ApplicationArchivesBuildItem applicationArchivesBuildItem,
final ClassInfo serviceClass) {
final var knownWorkflowModule = resolvedWorkflowModuleIds.get(serviceClass);
if (knownWorkflowModule != null) {
return knownWorkflowModule.getId();
}
// load workflow module ID from META-INF/workflow-module of the same JAR
// the workflow service class belongs to
final var containingArchive = applicationArchivesBuildItem.containingArchive(serviceClass.name());
final var workflowModuleInSameArchive = workflowModulesFound
.getWorkflowModules()
.entrySet()
.stream()
.filter(entry -> entry.getValue().equals(containingArchive))
.findFirst()
.map(Map.Entry::getKey);
if (workflowModuleInSameArchive.isPresent()) {
final var workflowModule = workflowModuleInSameArchive.get();
resolvedWorkflowModuleIds.put(serviceClass, workflowModule);
return workflowModule.getId();
}
// load workflow module ID from META-INF/workflow-module of the Java module JAR
// the workflow service class belongs to
// ==== NOT YET SUPPORTED ====
/*
if (serviceClass.module() != null) {
serviceClass.module().moduleInfoClass()
....
}
*/
// load workflow module ID from META-INF/workflow-module in classpath
// (this is suitable if the entire application is one workflow module):
final var rootArchive = applicationArchivesBuildItem.getRootArchive();
final var workflowModuleInRootArchive = workflowModulesFound
.getWorkflowModules()
.entrySet()
.stream()
.filter(entry -> entry.getValue().equals(rootArchive))
.findFirst()
.map(Map.Entry::getKey);
if (workflowModuleInRootArchive.isPresent()) {
final var workflowModule = workflowModuleInRootArchive.get();
resolvedWorkflowModuleIds.put(serviceClass, workflowModule);
return workflowModule.getId();
}
throw new IllegalStateException(
"""
No workflow module descriptor '%s' was found in any valid location:
- in JAR/directory of class '%s'
- in JAR/directory of Java module (%s) of class '%s'
- in global classpath"""
.formatted(
WorkflowModule.METAINF_WORKFLOWMODULE,
serviceClass.name(),
serviceClass.module() == null ? "if defined" : serviceClass.module().name(),
serviceClass.name()));
}
public static Optional<String> readWorkflowModuleDescriptor(
final URI descriptorInJar) {
try (final var descriptor = descriptorInJar
.toURL()
.openStream()) {
final var workflowModuleId = new String(descriptor.readAllBytes(), StandardCharsets.UTF_8);
final var trimmedWorkflowModuleId = workflowModuleId.trim();
if (trimmedWorkflowModuleId.isEmpty()) {
return Optional.empty();
}
return Optional.of(trimmedWorkflowModuleId);
} catch (IOException e) {
throw new IllegalStateException(
"Could not load workflow id from '%s'".formatted(descriptorInJar.toString()), e);
}
}
}