Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,58 @@
import java.util.Objects;

import io.quarkus.builder.item.MultiBuildItem;
import io.serverlessworkflow.api.types.Workflow;
import io.serverlessworkflow.impl.WorkflowDefinitionId;

/**
* Workflow file discovered during the build.
* <p>
* Holds the path to the workflow file, its namespace, and name.
* Holds the path to the workflow file, its namespace, name, and regular identifier.
*/
public final class DiscoveredWorkflowFileBuildItem extends MultiBuildItem {

private final Path workflowPath;
private final String namespace;
private final String name;
private final String identifier;

public DiscoveredWorkflowFileBuildItem(Path workflowPath, String namespace, String name) {
private final WorkflowDefinitionId workflowDefinitionId;
private final String regularIdentifier;

/**
* Constructs a new {@link DiscoveredWorkflowFileBuildItem} instance.
*
* @param workflowPath Path to the workflow file
* @param workflow {@link Workflow} instance representing the workflow
*/
public DiscoveredWorkflowFileBuildItem(Path workflowPath, Workflow workflow) {
this.workflowPath = workflowPath;
this.namespace = namespace;
this.name = name;
this.identifier = namespace + ":" + name;
this.workflowDefinitionId = WorkflowDefinitionId.of(workflow);
this.regularIdentifier = workflowDefinitionId.namespace() + ":" + workflowDefinitionId.name();
}

public String locationString() {
public String location() {
return this.workflowPath.toString();
}

public String namespace() {
return namespace;
return workflowDefinitionId.namespace();
}

public String name() {
return name;
return workflowDefinitionId.name();
}

public String identifier() {
return identifier;
public String regularIdentifier() {
return regularIdentifier;
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass())
return false;
DiscoveredWorkflowFileBuildItem that = (DiscoveredWorkflowFileBuildItem) o;
return Objects.equals(identifier, that.identifier);
return Objects.equals(regularIdentifier, that.regularIdentifier);
}

@Override
public int hashCode() {
return Objects.hash(identifier);
return Objects.hash(regularIdentifier);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ private static Set<DiscoveredWorkflowFileBuildItem> collectUniqueWorkflowFileDat

try (var stream = Files.walk(flowDir)) {
stream.filter(file -> Files.isRegularFile(file) && SUPPORTED_WORKFLOW_FILE_EXTENSIONS.stream()
.anyMatch(ext -> file.getFileName().toString().endsWith(ext))).forEach(consumeWorkflowFile(items));
.anyMatch(ext -> file.getFileName().toString().endsWith(ext)))
.forEach(consumeWorkflowFile(items));
} catch (IOException e) {
LOG.error("Failed to scan flow resources in path: {}", flowDir, e);
throw new UncheckedIOException(
Expand All @@ -71,8 +72,7 @@ private static Consumer<Path> consumeWorkflowFile(Set<DiscoveredWorkflowFileBuil
try {
Workflow workflow = WorkflowReader.readWorkflow(file);
DiscoveredWorkflowFileBuildItem buildItem = new DiscoveredWorkflowFileBuildItem(file,
workflow.getDocument().getNamespace(),
workflow.getDocument().getName());
workflow);
if (!workflowsSet.add(buildItem)) {
LOG.warn("Duplicate workflow detected: namespace='{}', name='{}'. The file at '{}' will be ignored.",
buildItem.namespace(), buildItem.name(), file.toAbsolutePath());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.quarkiverse.flow.deployment;

import java.util.Objects;
import java.util.Set;

import io.quarkus.builder.item.MultiBuildItem;

/**
* Build item representing a set of flow identifiers.
*/
public final class FlowIdentifierBuildItem extends MultiBuildItem {

private final Set<String> identifiers;

public FlowIdentifierBuildItem(Set<String> identifiers) {
this.identifiers = Objects.requireNonNull(identifiers, "'identifiers' must not be null");
}

public Set<String> identifiers() {
return identifiers;
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package io.quarkiverse.flow.deployment;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;

import org.objectweb.asm.Opcodes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.quarkiverse.flow.config.FlowDefinitionsConfig;
import io.quarkiverse.flow.config.FlowTracingConfig;
import io.quarkiverse.flow.providers.CredentialsProviderSecretManager;
import io.quarkiverse.flow.providers.HttpClientProvider;
Expand All @@ -19,19 +23,27 @@
import io.quarkiverse.flow.recorders.SDKRecorder;
import io.quarkiverse.flow.recorders.WorkflowApplicationRecorder;
import io.quarkiverse.flow.recorders.WorkflowDefinitionRecorder;
import io.quarkus.arc.Unremovable;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Produce;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.FieldCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem;
import io.serverlessworkflow.api.types.Workflow;
import io.serverlessworkflow.impl.WorkflowApplication;
import io.serverlessworkflow.impl.WorkflowDefinition;
import io.serverlessworkflow.impl.WorkflowException;
Expand Down Expand Up @@ -92,10 +104,8 @@ void registerWorkflowExceptionMapper(BuildProducer<ExceptionMapperBuildItem> map
@BuildStep
void produceWorkflowDefinitions(WorkflowDefinitionRecorder recorder,
BuildProducer<SyntheticBeanBuildItem> beans,
List<DiscoveredFlowBuildItem> discoveredFlows,
List<DiscoveredWorkflowFileBuildItem> workflows) {

List<String> identifiers = new ArrayList<>();
BuildProducer<FlowIdentifierBuildItem> identifiers,
List<DiscoveredFlowBuildItem> discoveredFlows) {

for (DiscoveredFlowBuildItem it : discoveredFlows) {
beans.produce(SyntheticBeanBuildItem.configure(WorkflowDefinition.class)
Expand All @@ -105,23 +115,76 @@ void produceWorkflowDefinitions(WorkflowDefinitionRecorder recorder,
.addQualifier().annotation(DotNames.IDENTIFIER).addValue("value", it.getClassName()).done()
.supplier(recorder.workflowDefinitionSupplier(it.getClassName()))
.done());
identifiers.add(it.getClassName());
identifiers.produce(new FlowIdentifierBuildItem(Set.of(it.getClassName())));
}
}

@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
void produceWorkflowDefinitionsFromFile(
List<DiscoveredWorkflowFileBuildItem> workflows,
BuildProducer<SyntheticBeanBuildItem> beans,
BuildProducer<FlowIdentifierBuildItem> identifiers,
WorkflowDefinitionRecorder recorder,
FlowDefinitionsConfig config) {
for (DiscoveredWorkflowFileBuildItem workflow : workflows) {

String flowSubclassIdentifier = WorkflowNamingConverter.generateFlowClassIdentifier(
workflow.namespace(), workflow.name(), config.namespace().prefix());

beans.produce(SyntheticBeanBuildItem.configure(WorkflowDefinition.class)
.scope(ApplicationScoped.class)
.unremovable()
.setRuntimeInit()
.addQualifier().annotation(DotNames.IDENTIFIER)
.addValue("value", workflow.identifier()).done()
.supplier(recorder.workflowDefinitionFromFileSupplier(workflow.locationString()))
.addValue("value", workflow.regularIdentifier()).done()
.addQualifier().annotation(DotNames.IDENTIFIER)
.addValue("value", flowSubclassIdentifier).done()
.supplier(recorder.workflowDefinitionFromFileSupplier(workflow.location()))
.done());

identifiers.add(workflow.identifier());
identifiers.produce(new FlowIdentifierBuildItem(
Set.of(flowSubclassIdentifier, workflow.regularIdentifier())));
}
}

@BuildStep
void produceGeneratedFlows(List<DiscoveredWorkflowFileBuildItem> workflows,
BuildProducer<GeneratedBeanBuildItem> classes,
FlowDefinitionsConfig definitionsConfig) {

GeneratedBeanGizmoAdaptor gizmo = new GeneratedBeanGizmoAdaptor(classes);
for (DiscoveredWorkflowFileBuildItem workflow : workflows) {
String flowSubclassIdentifier = WorkflowNamingConverter.generateFlowClassIdentifier(
workflow.namespace(), workflow.name(), definitionsConfig.namespace().prefix());

try (ClassCreator creator = ClassCreator.builder()
.className(flowSubclassIdentifier)
.superClass(DotNames.FLOW.toString())
.classOutput(gizmo)
.build()) {

creator.addAnnotation(Unremovable.class);
creator.addAnnotation(ApplicationScoped.class);
creator.addAnnotation(Identifier.class).add("value", flowSubclassIdentifier);

// workflowDefinition field
FieldCreator fieldCreator = creator.getFieldCreator("workflowDefinition",
WorkflowDefinition.class.getName());
fieldCreator.setModifiers(Opcodes.ACC_PUBLIC);
fieldCreator.addAnnotation(Inject.class);
fieldCreator.addAnnotation(Identifier.class)
.add("value", flowSubclassIdentifier);

logWorkflowList(identifiers);
// descriptor() method
var method = creator.getMethodCreator("descriptor", Workflow.class);
method.setModifiers(Opcodes.ACC_PUBLIC);
method.returnValue(
method.invokeVirtualMethod(
MethodDescriptor.ofMethod(WorkflowDefinition.class, "workflow", Workflow.class),
method.readInstanceField(fieldCreator.getFieldDescriptor(), method.getThis())));
}
}
}

@Record(ExecutionTime.RUNTIME_INIT)
Expand All @@ -144,6 +207,17 @@ void registerWorkflowApp(WorkflowApplicationRecorder recorder,
LOG.info("Flow: Registering Workflow Application bean: {}", WorkflowApplication.class.getName());
}

@BuildStep
@Produce(SyntheticBeanBuildItem.class)
void logRegisteredWorkflows(
List<FlowIdentifierBuildItem> registeredIdentifiers) {
List<String> allIdentifiers = registeredIdentifiers.stream().map(FlowIdentifierBuildItem::identifiers)
.map(set -> String.join(", ", set))
.distinct()
.collect(Collectors.toList());
logWorkflowList(allIdentifiers);
}

private void logWorkflowList(List<String> identifiers) {
if (identifiers.isEmpty()) {
LOG.info("Flow: No WorkflowDefinition beans were registered.");
Expand Down Expand Up @@ -184,7 +258,7 @@ public void watchChanges(List<DiscoveredWorkflowFileBuildItem> workflows,
BuildProducer<HotDeploymentWatchedFileBuildItem> watchedFiles) {
for (DiscoveredWorkflowFileBuildItem workflow : workflows) {
watchedFiles.produce(HotDeploymentWatchedFileBuildItem.builder()
.setLocation(workflow.locationString())
.setLocation(workflow.location())
.setRestartNeeded(true)
.build());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.quarkiverse.flow.deployment;

import java.util.Objects;
import java.util.Optional;

public interface WorkflowNamingConverter {

/**
* Converts a <code>document.namespace</code> from Workflow specification to a Java package name.
* <p>
* This method assumes that the provided namespace is valid according to the
* <a href="https://github.com/serverlessworkflow/specification/blob/main/schema/workflow.yaml">CNCF Specification</a>.
*
* @param namespace the CNCF <code>document.namespace</code> to convert
* @return the corresponding Java package name
*/
static String namespaceToPackage(String namespace) {
Objects.requireNonNull(namespace, "'namespace' must not be null");
return namespace.replace('-', '.').toLowerCase();
}

/**
* Converts a <code>document.name</code> from a Workflow Specification to a Java class name.
* <p>
* This method assumes that the provided name is valid according to the
* <a href="https://github.com/serverlessworkflow/specification/blob/main/schema/workflow.yaml">CNCF Specification</a>.
* <p>
* Example:
* <code>
* String className = WorkflowNamingConverter.nameToClassName("CNCFWorkflow");
* Assertions.assertEquals("CNCFWorkflow", className);
* </code>
*
* @param name the CNCF Workflow specification <code>document.name</code> to convert
* @return the corresponding Java class name
*/
static String nameToClassName(String name) {
Objects.requireNonNull(name, "'name' must not be null");
if (name.isBlank()) {
throw new IllegalArgumentException("'name' must not be empty");
}

StringBuilder classNameBuilder = new StringBuilder(name.length());

for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (c == '-') {
continue;
}
if (i == 0 || name.charAt(i - 1) == '-') {
classNameBuilder.append(Character.toUpperCase(c));
} else {
classNameBuilder.append(c);
}
}

return classNameBuilder.toString();
}

/**
* Generates a class identifier for {@link io.quarkiverse.flow.Flow} subclasses.
*
* @param namespace Document's namespace from specification
* @param name Document's name from specification
* @param namespaceFromConfig Base namespace for generating class identifiers
* @return the generated class identifier
*/
static String generateFlowClassIdentifier(String namespace, String name, Optional<String> namespaceFromConfig) {
return namespaceFromConfig.map(s -> String.format("%s.%s.%s", s, namespaceToPackage(namespace), nameToClassName(name)))
.orElseGet(() -> namespaceToPackage(namespace) + "." + nameToClassName(name));
}

}
Loading
Loading