Compare commits
5 Commits
a383de5a5f
...
d6b1571f18
| Author | SHA1 | Date | |
|---|---|---|---|
| d6b1571f18 | |||
| e617fb1754 | |||
| 06dc0ba641 | |||
| ba412b905e | |||
| 198c5d830e |
File diff suppressed because one or more lines are too long
@@ -54,6 +54,30 @@ public class ConstantResolver {
|
|||||||
return resolveManual(sn, context, visited);
|
return resolveManual(sn, context, visited);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
IMethodBinding mb = mi.resolveMethodBinding();
|
||||||
|
if (mb != null) {
|
||||||
|
ITypeBinding returnType = mb.getReturnType();
|
||||||
|
if (returnType != null) {
|
||||||
|
java.util.List<String> values = context.getEnumValues(returnType.getQualifiedName());
|
||||||
|
if (values != null && !values.isEmpty()) {
|
||||||
|
return "ENUM_SET:" + String.join(",", values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TypeDeclaration td = findEnclosingType(mi);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
|
||||||
|
if (md != null && md.getReturnType2() != null) {
|
||||||
|
java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString());
|
||||||
|
if (values != null && !values.isEmpty()) {
|
||||||
|
return "ENUM_SET:" + String.join(",", values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,20 @@ public class CallGraphBuilder {
|
|||||||
for (String calledMethod : calledMethods) {
|
for (String calledMethod : calledMethods) {
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||||
}
|
}
|
||||||
|
for (Object argObj : node.arguments()) {
|
||||||
|
if (argObj instanceof ExpressionMethodReference emr) {
|
||||||
|
String typeName = emr.getExpression().toString();
|
||||||
|
if ("this".equals(typeName) || "super".equals(typeName)) {
|
||||||
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
|
if (td != null) {
|
||||||
|
String refMethod = resolveMethodInType(td, emr.getName().getIdentifier());
|
||||||
|
if (refMethod != null) {
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return super.visit(node);
|
return super.visit(node);
|
||||||
}
|
}
|
||||||
@@ -216,6 +230,21 @@ public class CallGraphBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (expr instanceof ExpressionMethodReference emr) {
|
||||||
|
TypeDeclaration td = findEnclosingType(expr);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
for (Object stmtObj : md.getBody().statements()) {
|
||||||
|
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
|
||||||
|
expr = rs.getExpression();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
||||||
expr = traceVariable(expr);
|
expr = traceVariable(expr);
|
||||||
if (expr instanceof ClassInstanceCreation cic) {
|
if (expr instanceof ClassInstanceCreation cic) {
|
||||||
|
|||||||
@@ -47,12 +47,14 @@ public class GenericEventDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void processSendEvent(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
|
private void processSendEvent(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
|
||||||
TriggerPoint trigger = buildTriggerPoint(node, cu, null);
|
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, null);
|
||||||
if (trigger != null) {
|
if (builtTriggers != null) {
|
||||||
|
for (TriggerPoint trigger : builtTriggers) {
|
||||||
log.debug("Successfully built trigger point: {}", trigger.getEvent());
|
log.debug("Successfully built trigger point: {}", trigger.getEvent());
|
||||||
triggers.add(trigger);
|
triggers.add(trigger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void processHints(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
|
private void processHints(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
|
||||||
if (hints == null || hints.isEmpty()) return;
|
if (hints == null || hints.isEmpty()) return;
|
||||||
@@ -62,14 +64,16 @@ public class GenericEventDetector {
|
|||||||
|
|
||||||
for (LibraryHint hint : hints) {
|
for (LibraryHint hint : hints) {
|
||||||
if (calledMethod.equals(hint.getMethodFqn()) || isHintMatch(calledMethod, hint.getMethodFqn())) {
|
if (calledMethod.equals(hint.getMethodFqn()) || isHintMatch(calledMethod, hint.getMethodFqn())) {
|
||||||
TriggerPoint trigger = buildTriggerPoint(node, cu, hint.getEvent());
|
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, hint.getEvent());
|
||||||
if (trigger != null) {
|
if (builtTriggers != null) {
|
||||||
|
for (TriggerPoint trigger : builtTriggers) {
|
||||||
log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
|
log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
|
||||||
triggers.add(trigger);
|
triggers.add(trigger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isHintMatch(String called, String hintFqn) {
|
private boolean isHintMatch(String called, String hintFqn) {
|
||||||
return hintFqn.endsWith("." + called);
|
return hintFqn.endsWith("." + called);
|
||||||
@@ -102,12 +106,12 @@ public class GenericEventDetector {
|
|||||||
return receiver.toString() + "." + methodName;
|
return receiver.toString() + "." + methodName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TriggerPoint buildTriggerPoint(MethodInvocation node, CompilationUnit cu, String forcedEvent) {
|
private List<TriggerPoint> buildTriggerPoints(MethodInvocation node, CompilationUnit cu, String forcedEvent) {
|
||||||
String eventValue = null;
|
String eventValue = null;
|
||||||
if (forcedEvent != null) {
|
if (forcedEvent != null) {
|
||||||
eventValue = context.resolveString(forcedEvent);
|
eventValue = context.resolveString(forcedEvent);
|
||||||
} else {
|
} else {
|
||||||
if (node.arguments().isEmpty()) return null;
|
if (node.arguments().isEmpty()) return Collections.emptyList();
|
||||||
Expression eventExpr = (Expression) node.arguments().get(0);
|
Expression eventExpr = (Expression) node.arguments().get(0);
|
||||||
|
|
||||||
// Try MessageBuilder extraction first
|
// Try MessageBuilder extraction first
|
||||||
@@ -123,18 +127,37 @@ public class GenericEventDetector {
|
|||||||
MethodDeclaration method = findEnclosingMethod(node);
|
MethodDeclaration method = findEnclosingMethod(node);
|
||||||
TypeDeclaration type = findEnclosingType(node);
|
TypeDeclaration type = findEnclosingType(node);
|
||||||
|
|
||||||
if (type == null) return null;
|
if (type == null) return Collections.emptyList();
|
||||||
|
|
||||||
String sourceState = extractSourceState(node);
|
String sourceState = extractSourceState(node);
|
||||||
|
|
||||||
return TriggerPoint.builder()
|
List<TriggerPoint> results = new ArrayList<>();
|
||||||
|
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
|
||||||
|
String[] parts = eventValue.substring(9).split(",");
|
||||||
|
for (String part : parts) {
|
||||||
|
if (!part.trim().isEmpty()) {
|
||||||
|
results.add(TriggerPoint.builder()
|
||||||
|
.event(part.trim())
|
||||||
|
.sourceState(sourceState)
|
||||||
|
.className(context.getFqn(type))
|
||||||
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.add(TriggerPoint.builder()
|
||||||
.event(eventValue)
|
.event(eventValue)
|
||||||
.sourceState(sourceState)
|
.sourceState(sourceState)
|
||||||
.className(context.getFqn(type))
|
.className(context.getFqn(type))
|
||||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
.build();
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractSourceState(ASTNode node) {
|
private String extractSourceState(ASTNode node) {
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ public class CodebaseContext {
|
|||||||
private final List<CompilationUnit> allCus = new ArrayList<>();
|
private final List<CompilationUnit> allCus = new ArrayList<>();
|
||||||
|
|
||||||
private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
|
private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
|
||||||
|
private final Map<String, List<String>> enumValues = new HashMap<>();
|
||||||
|
|
||||||
public void scan(Set<Path> rootDirs, Set<String> customIgnorePatterns) throws IOException {
|
public void scan(Set<Path> rootDirs, Set<String> customIgnorePatterns) throws IOException {
|
||||||
this.allProperties = propertyResolver.resolveAllProperties(rootDirs);
|
this.allProperties = propertyResolver.resolveAllProperties(rootDirs);
|
||||||
@@ -188,6 +189,8 @@ public class CodebaseContext {
|
|||||||
for (Object type : cu.types()) {
|
for (Object type : cu.types()) {
|
||||||
if (type instanceof TypeDeclaration td) {
|
if (type instanceof TypeDeclaration td) {
|
||||||
indexType(td, packageName, javaFile);
|
indexType(td, packageName, javaFile);
|
||||||
|
} else if (type instanceof EnumDeclaration ed) {
|
||||||
|
indexEnum(ed, packageName, javaFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,6 +251,35 @@ public class CodebaseContext {
|
|||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void indexEnum(EnumDeclaration ed, String parentFqn, Path javaFile) {
|
||||||
|
String simpleName = ed.getName().getIdentifier();
|
||||||
|
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
||||||
|
|
||||||
|
classes.put(fqn, (CompilationUnit) ed.getRoot());
|
||||||
|
classPaths.put(fqn, javaFile);
|
||||||
|
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
for (Object o : ed.enumConstants()) {
|
||||||
|
if (o instanceof EnumConstantDeclaration ecd) {
|
||||||
|
values.add(fqn + "." + ecd.getName().getIdentifier());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enumValues.put(fqn, values);
|
||||||
|
if (!simpleNameToFqn.containsKey(simpleName)) {
|
||||||
|
simpleNameToFqn.put(simpleName, fqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getEnumValues(String fqnOrSimpleName) {
|
||||||
|
if (fqnOrSimpleName == null) return null;
|
||||||
|
List<String> values = enumValues.get(fqnOrSimpleName);
|
||||||
|
if (values != null) return values;
|
||||||
|
String fqn = simpleNameToFqn.get(fqnOrSimpleName);
|
||||||
|
if (fqn != null) return enumValues.get(fqn);
|
||||||
|
// Also try stripping array or generic types if any (e.g. MyEnum[])
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public State resolveState(Expression expr, CompilationUnit cu) {
|
public State resolveState(Expression expr, CompilationUnit cu) {
|
||||||
if (expr == null)
|
if (expr == null)
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -137,6 +137,12 @@ public class RegressionTest {
|
|||||||
root.resolve("state_machines/maven_multi_module/core-module"),
|
root.resolve("state_machines/maven_multi_module/core-module"),
|
||||||
Path.of("src/test/resources/golden/MavenOrderStateMachine"),
|
Path.of("src/test/resources/golden/MavenOrderStateMachine"),
|
||||||
"MavenOrderStateMachine"
|
"MavenOrderStateMachine"
|
||||||
|
),
|
||||||
|
new TestScenario(
|
||||||
|
"Complex Multi-Module Sample",
|
||||||
|
root.resolve("state_machines/complex_multi_module_sm"),
|
||||||
|
Path.of("src/test/resources/golden/StateMachineConfig"),
|
||||||
|
"StateMachineConfig"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,4 +44,42 @@ public class GenericEventDetectorTest {
|
|||||||
assertThat(triggers.get(0).getEvent()).isEqualTo("myEvent");
|
assertThat(triggers.get(0).getEvent()).isEqualTo("myEvent");
|
||||||
assertThat(triggers.get(0).getMethodName()).isEqualTo("a");
|
assertThat(triggers.get(0).getMethodName()).isEqualTo("a");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEnumGetterUnionExtraction(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("MyEnum.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public enum MyEnum {\n" +
|
||||||
|
" STATE_A, STATE_B;\n" +
|
||||||
|
"}\n");
|
||||||
|
|
||||||
|
Files.writeString(dir.resolve("MyService.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"import org.springframework.statemachine.StateMachine;\n" +
|
||||||
|
"public class MyService {\n" +
|
||||||
|
" private StateMachine<String, String> stateMachine;\n" +
|
||||||
|
" public MyEnum getEvent() { return MyEnum.STATE_A; }\n" + // Pretend it returns the enum
|
||||||
|
" public void trigger() {\n" +
|
||||||
|
" stateMachine.sendEvent(this.getEvent());\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}\n");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), List.of());
|
||||||
|
|
||||||
|
CompilationUnit serviceCu = context.getCompilationUnits().stream()
|
||||||
|
.filter(cu -> cu.getJavaElement() == null || cu.getJavaElement().getElementName().contains("MyService"))
|
||||||
|
.findFirst().orElseThrow();
|
||||||
|
|
||||||
|
List<TriggerPoint> triggers = detector.detect(serviceCu);
|
||||||
|
|
||||||
|
System.out.println("TRIGGERS: " + triggers);
|
||||||
|
assertThat(triggers).hasSize(2);
|
||||||
|
assertThat(triggers).anyMatch(t -> t.getEvent().equals("com.example.MyEnum.STATE_A"));
|
||||||
|
assertThat(triggers).anyMatch(t -> t.getEvent().equals("com.example.MyEnum.STATE_B"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.exporter;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.model.State;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
import click.kamil.springstatemachineexporter.model.TransitionType;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Guard;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Action;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class PlantUmlTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExportSanitizesLabels() {
|
||||||
|
PlantUml plantUml = new PlantUml();
|
||||||
|
ExportOptions options = ExportOptions.builder()
|
||||||
|
.embedIdentifiers(true)
|
||||||
|
.useLambdaGuards(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
State state1 = State.of("State1");
|
||||||
|
State state2 = State.of("State2");
|
||||||
|
|
||||||
|
Transition t = new Transition();
|
||||||
|
t.setType(TransitionType.EXTERNAL);
|
||||||
|
t.setSourceStates(List.of(state1));
|
||||||
|
t.setTargetStates(List.of(state2));
|
||||||
|
|
||||||
|
// Add a guard and action containing problematic characters
|
||||||
|
t.setGuard(Guard.of("list.size() < 5", false, null, null, null, null));
|
||||||
|
t.setActions(List.of(Action.of("doSomething(new int[]{1, 2})", false, null, null, null, null)));
|
||||||
|
|
||||||
|
String result = plantUml.export(
|
||||||
|
"TestMachine",
|
||||||
|
List.of(t),
|
||||||
|
Set.of("State1"),
|
||||||
|
Set.of("State2"),
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that < and > are replaced with HTML entities
|
||||||
|
assertThat(result).doesNotContain("< 5");
|
||||||
|
assertThat(result).contains("< 5");
|
||||||
|
|
||||||
|
// Verify that [ and ] are replaced with HTML entities inside the link
|
||||||
|
assertThat(result).doesNotContain("[list.size() < 5]");
|
||||||
|
assertThat(result).doesNotContain("new int[]{1, 2}");
|
||||||
|
assertThat(result).contains("[list.size() < 5]");
|
||||||
|
assertThat(result).contains("new int[]{1, 2}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
digraph statemachine {
|
||||||
|
rankdir=LR;
|
||||||
|
node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
|
||||||
|
edge [fontname="Arial", fontsize=10];
|
||||||
|
|
||||||
|
_start [shape=circle, label="", fillcolor=black, width=0.1];
|
||||||
|
_start -> NEW;
|
||||||
|
COMPLETED [fillcolor=lightgray];
|
||||||
|
CANCELLED [fillcolor=lightgray];
|
||||||
|
NEW -> PROCESSING [label="OrderEvent.PROCESS", style="solid", color="black"];
|
||||||
|
PROCESSING -> COMPLETED [label="OrderEvent.COMPLETE", style="solid", color="black"];
|
||||||
|
NEW -> CANCELLED [label="OrderEvent.CANCEL", style="solid", color="black"];
|
||||||
|
PROCESSING -> CANCELLED [label="OrderEvent.CANCEL", style="solid", color="black"];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 69
|
"lineNumber" : 78
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -61,6 +61,17 @@
|
|||||||
"verb" : "POST"
|
"verb" : "POST"
|
||||||
},
|
},
|
||||||
"parameters" : [ ]
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/orders/functional-complete",
|
||||||
|
"className" : "click.kamil.web.OrderController",
|
||||||
|
"methodName" : "functionalCompleteOrder",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/orders/functional-complete",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
}, {
|
}, {
|
||||||
"type" : "RABBIT",
|
"type" : "RABBIT",
|
||||||
"name" : "RABBIT: order.custom.transition.queue",
|
"name" : "RABBIT: order.custom.transition.queue",
|
||||||
@@ -201,6 +212,36 @@
|
|||||||
"targetState" : "OrderState.CANCELLED",
|
"targetState" : "OrderState.CANCELLED",
|
||||||
"event" : "OrderEvent.CANCEL"
|
"event" : "OrderEvent.CANCEL"
|
||||||
} ]
|
} ]
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/orders/functional-complete",
|
||||||
|
"className" : "click.kamil.web.OrderController",
|
||||||
|
"methodName" : "functionalCompleteOrder",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/orders/functional-complete",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.web.OrderController.functionalCompleteOrder", "click.kamil.web.OrderController.triggerTransitionFunction", "click.kamil.service.StateMachineServiceImpl.sendMessage" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "OrderEvent.COMPLETE",
|
||||||
|
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||||
|
"methodName" : "sendMessage",
|
||||||
|
"sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 25
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : [ {
|
||||||
|
"sourceState" : "OrderState.PROCESSING",
|
||||||
|
"targetState" : "OrderState.COMPLETED",
|
||||||
|
"event" : "OrderEvent.COMPLETE"
|
||||||
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "RABBIT",
|
"type" : "RABBIT",
|
||||||
@@ -227,7 +268,7 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 69
|
"lineNumber" : 78
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@startuml
|
||||||
|
!pragma layout smetana
|
||||||
|
set separator none
|
||||||
|
hide empty description
|
||||||
|
hide stereotype
|
||||||
|
skinparam state {
|
||||||
|
BackgroundColor white
|
||||||
|
BorderColor #94a3b8
|
||||||
|
BorderThickness 1
|
||||||
|
FontName Inter
|
||||||
|
FontSize 9
|
||||||
|
FontStyle bold
|
||||||
|
RoundCorner 20
|
||||||
|
Padding 1
|
||||||
|
}
|
||||||
|
skinparam shadowing false
|
||||||
|
skinparam ArrowFontName JetBrains Mono
|
||||||
|
skinparam ArrowFontSize 8
|
||||||
|
skinparam ArrowColor #cbd5e1
|
||||||
|
skinparam ArrowThickness 1
|
||||||
|
skinparam dpi 110
|
||||||
|
skinparam svgLinkTarget _self
|
||||||
|
|
||||||
|
[*] --> OrderState.NEW
|
||||||
|
|
||||||
|
|
||||||
|
OrderState.NEW -[#1E90FF,bold]-> OrderState.PROCESSING <<external>> : OrderEvent.PROCESS
|
||||||
|
OrderState.PROCESSING -[#1E90FF,bold]-> OrderState.COMPLETED <<external>> : OrderEvent.COMPLETE
|
||||||
|
OrderState.NEW -[#1E90FF,bold]-> OrderState.CANCELLED <<external>> : OrderEvent.CANCEL
|
||||||
|
OrderState.PROCESSING -[#1E90FF,bold]-> OrderState.CANCELLED <<external>> : OrderEvent.CANCEL
|
||||||
|
|
||||||
|
OrderState.COMPLETED --> [*]
|
||||||
|
OrderState.CANCELLED --> [*]
|
||||||
|
@enduml
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="NEW">
|
||||||
|
<state id="NEW">
|
||||||
|
<transition target="PROCESSING" event="OrderEvent.PROCESS"/>
|
||||||
|
<transition target="CANCELLED" event="OrderEvent.CANCEL"/>
|
||||||
|
</state>
|
||||||
|
<state id="PROCESSING">
|
||||||
|
<transition target="COMPLETED" event="OrderEvent.COMPLETE"/>
|
||||||
|
<transition target="CANCELLED" event="OrderEvent.CANCEL"/>
|
||||||
|
</state>
|
||||||
|
<state id="COMPLETED">
|
||||||
|
</state>
|
||||||
|
<state id="CANCELLED">
|
||||||
|
</state>
|
||||||
|
</scxml>
|
||||||
|
|
||||||
@@ -14,6 +14,10 @@ public interface StateMachineService {
|
|||||||
// Quirky pattern 2: Generic Varargs
|
// Quirky pattern 2: Generic Varargs
|
||||||
<T extends IEvent> void sendEventsVarargs(T... events);
|
<T extends IEvent> void sendEventsVarargs(T... events);
|
||||||
|
|
||||||
|
// Functional quirks: passing functions and method references
|
||||||
|
<T extends IEvent> void executeFunctionalTransition(java.util.function.Supplier<T> eventSupplier,
|
||||||
|
java.util.function.Function<T, Void> transitionAction);
|
||||||
|
|
||||||
// Child class of Spring's GenericMessage with a generic payload
|
// Child class of Spring's GenericMessage with a generic payload
|
||||||
<T extends click.kamil.domain.OrderEvent, P> void sendCustomMessage(click.kamil.domain.CustomStateMachineMessage<T, P> customMessage);
|
<T extends click.kamil.domain.OrderEvent, P> void sendCustomMessage(click.kamil.domain.CustomStateMachineMessage<T, P> customMessage);
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,15 @@ public class StateMachineServiceImpl implements StateMachineService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends IEvent> void executeFunctionalTransition(java.util.function.Supplier<T> eventSupplier,
|
||||||
|
java.util.function.Function<T, Void> transitionAction) {
|
||||||
|
// Resolve the event from the getter / supplier function
|
||||||
|
T event = eventSupplier.get();
|
||||||
|
// Apply the transition via the provided function / method reference
|
||||||
|
transitionAction.apply(event);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T extends OrderEvent, P> void sendCustomMessage(click.kamil.domain.CustomStateMachineMessage<T, P> customMessage) {
|
public <T extends OrderEvent, P> void sendCustomMessage(click.kamil.domain.CustomStateMachineMessage<T, P> customMessage) {
|
||||||
// Because CustomStateMachineMessage extends GenericMessage<T> and implements Message<T>,
|
// Because CustomStateMachineMessage extends GenericMessage<T> and implements Message<T>,
|
||||||
|
|||||||
@@ -34,4 +34,27 @@ public class OrderController {
|
|||||||
stateMachineService.sendMessage(OrderEvent.CANCEL);
|
stateMachineService.sendMessage(OrderEvent.CANCEL);
|
||||||
return "Order cancel event sent";
|
return "Order cancel event sent";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Functional Quirks for Static Analysis ---
|
||||||
|
|
||||||
|
// A getter returning a specific ENUM constant
|
||||||
|
public OrderEvent getCompleteEvent() {
|
||||||
|
return OrderEvent.COMPLETE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A specific function that performs the transition
|
||||||
|
public Void triggerTransitionFunction(click.kamil.domain.IEvent event) {
|
||||||
|
stateMachineService.sendMessage(event);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/functional-complete")
|
||||||
|
public String functionalCompleteOrder() {
|
||||||
|
// Pass the getter and the transition applier as method references
|
||||||
|
stateMachineService.executeFunctionalTransition(
|
||||||
|
this::getCompleteEvent,
|
||||||
|
this::triggerTransitionFunction
|
||||||
|
);
|
||||||
|
return "Functional complete event triggered via method references";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user