Skip to content

Commit 6535cfd

Browse files
authored
Generate Flow bean from Workflow file (#92)
* Generate Flow bean from Workflow file Signed-off-by: Matheus Cruz <[email protected]> * Use WorkflowDefinitionId to hold workflow data Signed-off-by: Matheus Cruz <[email protected]> --------- Signed-off-by: Matheus Cruz <[email protected]>
1 parent 80d0551 commit 6535cfd

File tree

16 files changed

+510
-81
lines changed

16 files changed

+510
-81
lines changed

core/deployment/src/main/java/io/quarkiverse/flow/deployment/DiscoveredWorkflowFileBuildItem.java

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,58 @@
44
import java.util.Objects;
55

66
import io.quarkus.builder.item.MultiBuildItem;
7+
import io.serverlessworkflow.api.types.Workflow;
8+
import io.serverlessworkflow.impl.WorkflowDefinitionId;
79

810
/**
911
* Workflow file discovered during the build.
1012
* <p>
11-
* Holds the path to the workflow file, its namespace, and name.
13+
* Holds the path to the workflow file, its namespace, name, and regular identifier.
1214
*/
1315
public final class DiscoveredWorkflowFileBuildItem extends MultiBuildItem {
1416

1517
private final Path workflowPath;
16-
private final String namespace;
17-
private final String name;
18-
private final String identifier;
19-
20-
public DiscoveredWorkflowFileBuildItem(Path workflowPath, String namespace, String name) {
18+
private final WorkflowDefinitionId workflowDefinitionId;
19+
private final String regularIdentifier;
20+
21+
/**
22+
* Constructs a new {@link DiscoveredWorkflowFileBuildItem} instance.
23+
*
24+
* @param workflowPath Path to the workflow file
25+
* @param workflow {@link Workflow} instance representing the workflow
26+
*/
27+
public DiscoveredWorkflowFileBuildItem(Path workflowPath, Workflow workflow) {
2128
this.workflowPath = workflowPath;
22-
this.namespace = namespace;
23-
this.name = name;
24-
this.identifier = namespace + ":" + name;
29+
this.workflowDefinitionId = WorkflowDefinitionId.of(workflow);
30+
this.regularIdentifier = workflowDefinitionId.namespace() + ":" + workflowDefinitionId.name();
2531
}
2632

27-
public String locationString() {
33+
public String location() {
2834
return this.workflowPath.toString();
2935
}
3036

3137
public String namespace() {
32-
return namespace;
38+
return workflowDefinitionId.namespace();
3339
}
3440

3541
public String name() {
36-
return name;
42+
return workflowDefinitionId.name();
3743
}
3844

39-
public String identifier() {
40-
return identifier;
45+
public String regularIdentifier() {
46+
return regularIdentifier;
4147
}
4248

4349
@Override
4450
public boolean equals(Object o) {
4551
if (o == null || getClass() != o.getClass())
4652
return false;
4753
DiscoveredWorkflowFileBuildItem that = (DiscoveredWorkflowFileBuildItem) o;
48-
return Objects.equals(identifier, that.identifier);
54+
return Objects.equals(regularIdentifier, that.regularIdentifier);
4955
}
5056

5157
@Override
5258
public int hashCode() {
53-
return Objects.hash(identifier);
59+
return Objects.hash(regularIdentifier);
5460
}
5561
}

core/deployment/src/main/java/io/quarkiverse/flow/deployment/FlowCollectorProcessor.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ private static Set<DiscoveredWorkflowFileBuildItem> collectUniqueWorkflowFileDat
5757

5858
try (var stream = Files.walk(flowDir)) {
5959
stream.filter(file -> Files.isRegularFile(file) && SUPPORTED_WORKFLOW_FILE_EXTENSIONS.stream()
60-
.anyMatch(ext -> file.getFileName().toString().endsWith(ext))).forEach(consumeWorkflowFile(items));
60+
.anyMatch(ext -> file.getFileName().toString().endsWith(ext)))
61+
.forEach(consumeWorkflowFile(items));
6162
} catch (IOException e) {
6263
LOG.error("Failed to scan flow resources in path: {}", flowDir, e);
6364
throw new UncheckedIOException(
@@ -71,8 +72,7 @@ private static Consumer<Path> consumeWorkflowFile(Set<DiscoveredWorkflowFileBuil
7172
try {
7273
Workflow workflow = WorkflowReader.readWorkflow(file);
7374
DiscoveredWorkflowFileBuildItem buildItem = new DiscoveredWorkflowFileBuildItem(file,
74-
workflow.getDocument().getNamespace(),
75-
workflow.getDocument().getName());
75+
workflow);
7676
if (!workflowsSet.add(buildItem)) {
7777
LOG.warn("Duplicate workflow detected: namespace='{}', name='{}'. The file at '{}' will be ignored.",
7878
buildItem.namespace(), buildItem.name(), file.toAbsolutePath());
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.quarkiverse.flow.deployment;
2+
3+
import java.util.Objects;
4+
import java.util.Set;
5+
6+
import io.quarkus.builder.item.MultiBuildItem;
7+
8+
/**
9+
* Build item representing a set of flow identifiers.
10+
*/
11+
public final class FlowIdentifierBuildItem extends MultiBuildItem {
12+
13+
private final Set<String> identifiers;
14+
15+
public FlowIdentifierBuildItem(Set<String> identifiers) {
16+
this.identifiers = Objects.requireNonNull(identifiers, "'identifiers' must not be null");
17+
}
18+
19+
public Set<String> identifiers() {
20+
return identifiers;
21+
}
22+
}

core/deployment/src/main/java/io/quarkiverse/flow/deployment/FlowProcessor.java

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package io.quarkiverse.flow.deployment;
22

3-
import java.util.ArrayList;
43
import java.util.Collections;
54
import java.util.List;
5+
import java.util.Set;
6+
import java.util.stream.Collectors;
67

78
import jakarta.enterprise.context.ApplicationScoped;
9+
import jakarta.inject.Inject;
810
import jakarta.ws.rs.Priorities;
911

12+
import org.objectweb.asm.Opcodes;
1013
import org.slf4j.Logger;
1114
import org.slf4j.LoggerFactory;
1215

16+
import io.quarkiverse.flow.config.FlowDefinitionsConfig;
1317
import io.quarkiverse.flow.config.FlowTracingConfig;
1418
import io.quarkiverse.flow.providers.CredentialsProviderSecretManager;
1519
import io.quarkiverse.flow.providers.HttpClientProvider;
@@ -19,19 +23,27 @@
1923
import io.quarkiverse.flow.recorders.SDKRecorder;
2024
import io.quarkiverse.flow.recorders.WorkflowApplicationRecorder;
2125
import io.quarkiverse.flow.recorders.WorkflowDefinitionRecorder;
26+
import io.quarkus.arc.Unremovable;
2227
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
28+
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
29+
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
2330
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
2431
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
2532
import io.quarkus.deployment.IsDevelopment;
2633
import io.quarkus.deployment.annotations.BuildProducer;
2734
import io.quarkus.deployment.annotations.BuildStep;
2835
import io.quarkus.deployment.annotations.ExecutionTime;
36+
import io.quarkus.deployment.annotations.Produce;
2937
import io.quarkus.deployment.annotations.Record;
3038
import io.quarkus.deployment.builditem.FeatureBuildItem;
3139
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;
3240
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
3341
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
42+
import io.quarkus.gizmo.ClassCreator;
43+
import io.quarkus.gizmo.FieldCreator;
44+
import io.quarkus.gizmo.MethodDescriptor;
3445
import io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem;
46+
import io.serverlessworkflow.api.types.Workflow;
3547
import io.serverlessworkflow.impl.WorkflowApplication;
3648
import io.serverlessworkflow.impl.WorkflowDefinition;
3749
import io.serverlessworkflow.impl.WorkflowException;
@@ -92,10 +104,8 @@ void registerWorkflowExceptionMapper(BuildProducer<ExceptionMapperBuildItem> map
92104
@BuildStep
93105
void produceWorkflowDefinitions(WorkflowDefinitionRecorder recorder,
94106
BuildProducer<SyntheticBeanBuildItem> beans,
95-
List<DiscoveredFlowBuildItem> discoveredFlows,
96-
List<DiscoveredWorkflowFileBuildItem> workflows) {
97-
98-
List<String> identifiers = new ArrayList<>();
107+
BuildProducer<FlowIdentifierBuildItem> identifiers,
108+
List<DiscoveredFlowBuildItem> discoveredFlows) {
99109

100110
for (DiscoveredFlowBuildItem it : discoveredFlows) {
101111
beans.produce(SyntheticBeanBuildItem.configure(WorkflowDefinition.class)
@@ -105,23 +115,76 @@ void produceWorkflowDefinitions(WorkflowDefinitionRecorder recorder,
105115
.addQualifier().annotation(DotNames.IDENTIFIER).addValue("value", it.getClassName()).done()
106116
.supplier(recorder.workflowDefinitionSupplier(it.getClassName()))
107117
.done());
108-
identifiers.add(it.getClassName());
118+
identifiers.produce(new FlowIdentifierBuildItem(Set.of(it.getClassName())));
109119
}
120+
}
110121

122+
@BuildStep
123+
@Record(ExecutionTime.RUNTIME_INIT)
124+
void produceWorkflowDefinitionsFromFile(
125+
List<DiscoveredWorkflowFileBuildItem> workflows,
126+
BuildProducer<SyntheticBeanBuildItem> beans,
127+
BuildProducer<FlowIdentifierBuildItem> identifiers,
128+
WorkflowDefinitionRecorder recorder,
129+
FlowDefinitionsConfig config) {
111130
for (DiscoveredWorkflowFileBuildItem workflow : workflows) {
131+
132+
String flowSubclassIdentifier = WorkflowNamingConverter.generateFlowClassIdentifier(
133+
workflow.namespace(), workflow.name(), config.namespace().prefix());
134+
112135
beans.produce(SyntheticBeanBuildItem.configure(WorkflowDefinition.class)
113136
.scope(ApplicationScoped.class)
114137
.unremovable()
115138
.setRuntimeInit()
116139
.addQualifier().annotation(DotNames.IDENTIFIER)
117-
.addValue("value", workflow.identifier()).done()
118-
.supplier(recorder.workflowDefinitionFromFileSupplier(workflow.locationString()))
140+
.addValue("value", workflow.regularIdentifier()).done()
141+
.addQualifier().annotation(DotNames.IDENTIFIER)
142+
.addValue("value", flowSubclassIdentifier).done()
143+
.supplier(recorder.workflowDefinitionFromFileSupplier(workflow.location()))
119144
.done());
120145

121-
identifiers.add(workflow.identifier());
146+
identifiers.produce(new FlowIdentifierBuildItem(
147+
Set.of(flowSubclassIdentifier, workflow.regularIdentifier())));
122148
}
149+
}
150+
151+
@BuildStep
152+
void produceGeneratedFlows(List<DiscoveredWorkflowFileBuildItem> workflows,
153+
BuildProducer<GeneratedBeanBuildItem> classes,
154+
FlowDefinitionsConfig definitionsConfig) {
155+
156+
GeneratedBeanGizmoAdaptor gizmo = new GeneratedBeanGizmoAdaptor(classes);
157+
for (DiscoveredWorkflowFileBuildItem workflow : workflows) {
158+
String flowSubclassIdentifier = WorkflowNamingConverter.generateFlowClassIdentifier(
159+
workflow.namespace(), workflow.name(), definitionsConfig.namespace().prefix());
160+
161+
try (ClassCreator creator = ClassCreator.builder()
162+
.className(flowSubclassIdentifier)
163+
.superClass(DotNames.FLOW.toString())
164+
.classOutput(gizmo)
165+
.build()) {
166+
167+
creator.addAnnotation(Unremovable.class);
168+
creator.addAnnotation(ApplicationScoped.class);
169+
creator.addAnnotation(Identifier.class).add("value", flowSubclassIdentifier);
170+
171+
// workflowDefinition field
172+
FieldCreator fieldCreator = creator.getFieldCreator("workflowDefinition",
173+
WorkflowDefinition.class.getName());
174+
fieldCreator.setModifiers(Opcodes.ACC_PUBLIC);
175+
fieldCreator.addAnnotation(Inject.class);
176+
fieldCreator.addAnnotation(Identifier.class)
177+
.add("value", flowSubclassIdentifier);
123178

124-
logWorkflowList(identifiers);
179+
// descriptor() method
180+
var method = creator.getMethodCreator("descriptor", Workflow.class);
181+
method.setModifiers(Opcodes.ACC_PUBLIC);
182+
method.returnValue(
183+
method.invokeVirtualMethod(
184+
MethodDescriptor.ofMethod(WorkflowDefinition.class, "workflow", Workflow.class),
185+
method.readInstanceField(fieldCreator.getFieldDescriptor(), method.getThis())));
186+
}
187+
}
125188
}
126189

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

210+
@BuildStep
211+
@Produce(SyntheticBeanBuildItem.class)
212+
void logRegisteredWorkflows(
213+
List<FlowIdentifierBuildItem> registeredIdentifiers) {
214+
List<String> allIdentifiers = registeredIdentifiers.stream().map(FlowIdentifierBuildItem::identifiers)
215+
.map(set -> String.join(", ", set))
216+
.distinct()
217+
.collect(Collectors.toList());
218+
logWorkflowList(allIdentifiers);
219+
}
220+
147221
private void logWorkflowList(List<String> identifiers) {
148222
if (identifiers.isEmpty()) {
149223
LOG.info("Flow: No WorkflowDefinition beans were registered.");
@@ -184,7 +258,7 @@ public void watchChanges(List<DiscoveredWorkflowFileBuildItem> workflows,
184258
BuildProducer<HotDeploymentWatchedFileBuildItem> watchedFiles) {
185259
for (DiscoveredWorkflowFileBuildItem workflow : workflows) {
186260
watchedFiles.produce(HotDeploymentWatchedFileBuildItem.builder()
187-
.setLocation(workflow.locationString())
261+
.setLocation(workflow.location())
188262
.setRestartNeeded(true)
189263
.build());
190264
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.quarkiverse.flow.deployment;
2+
3+
import java.util.Objects;
4+
import java.util.Optional;
5+
6+
public interface WorkflowNamingConverter {
7+
8+
/**
9+
* Converts a <code>document.namespace</code> from Workflow specification to a Java package name.
10+
* <p>
11+
* This method assumes that the provided namespace is valid according to the
12+
* <a href="https://github.com/serverlessworkflow/specification/blob/main/schema/workflow.yaml">CNCF Specification</a>.
13+
*
14+
* @param namespace the CNCF <code>document.namespace</code> to convert
15+
* @return the corresponding Java package name
16+
*/
17+
static String namespaceToPackage(String namespace) {
18+
Objects.requireNonNull(namespace, "'namespace' must not be null");
19+
return namespace.replace('-', '.').toLowerCase();
20+
}
21+
22+
/**
23+
* Converts a <code>document.name</code> from a Workflow Specification to a Java class name.
24+
* <p>
25+
* This method assumes that the provided name is valid according to the
26+
* <a href="https://github.com/serverlessworkflow/specification/blob/main/schema/workflow.yaml">CNCF Specification</a>.
27+
* <p>
28+
* Example:
29+
* <code>
30+
* String className = WorkflowNamingConverter.nameToClassName("CNCFWorkflow");
31+
* Assertions.assertEquals("CNCFWorkflow", className);
32+
* </code>
33+
*
34+
* @param name the CNCF Workflow specification <code>document.name</code> to convert
35+
* @return the corresponding Java class name
36+
*/
37+
static String nameToClassName(String name) {
38+
Objects.requireNonNull(name, "'name' must not be null");
39+
if (name.isBlank()) {
40+
throw new IllegalArgumentException("'name' must not be empty");
41+
}
42+
43+
StringBuilder classNameBuilder = new StringBuilder(name.length());
44+
45+
for (int i = 0; i < name.length(); i++) {
46+
char c = name.charAt(i);
47+
if (c == '-') {
48+
continue;
49+
}
50+
if (i == 0 || name.charAt(i - 1) == '-') {
51+
classNameBuilder.append(Character.toUpperCase(c));
52+
} else {
53+
classNameBuilder.append(c);
54+
}
55+
}
56+
57+
return classNameBuilder.toString();
58+
}
59+
60+
/**
61+
* Generates a class identifier for {@link io.quarkiverse.flow.Flow} subclasses.
62+
*
63+
* @param namespace Document's namespace from specification
64+
* @param name Document's name from specification
65+
* @param namespaceFromConfig Base namespace for generating class identifiers
66+
* @return the generated class identifier
67+
*/
68+
static String generateFlowClassIdentifier(String namespace, String name, Optional<String> namespaceFromConfig) {
69+
return namespaceFromConfig.map(s -> String.format("%s.%s.%s", s, namespaceToPackage(namespace), nameToClassName(name)))
70+
.orElseGet(() -> namespaceToPackage(namespace) + "." + nameToClassName(name));
71+
}
72+
73+
}

0 commit comments

Comments
 (0)