removal and better style
This commit is contained in:
@@ -2,13 +2,9 @@ package click.kamil.springstatemachineexporter;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser;
|
||||
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
|
||||
import click.kamil.springstatemachineexporter.ast.app.StateMachineStateConfigurationMethodFinder;
|
||||
import click.kamil.springstatemachineexporter.ast.app.StateMachineTransitionConfigurationMethodFinder;
|
||||
import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils;
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.ast.common.FileUtils;
|
||||
import click.kamil.springstatemachineexporter.ast.in.AstFileFinder;
|
||||
import click.kamil.springstatemachineexporter.ast.out.Dot;
|
||||
import click.kamil.springstatemachineexporter.ast.out.PlantUml;
|
||||
import click.kamil.springstatemachineexporter.ast.out.Scxml;
|
||||
@@ -27,7 +23,6 @@ import java.util.Set;
|
||||
public class Main {
|
||||
private static final String START_DIR = ".";
|
||||
private static final String OUTPUT_DIR = "./out";
|
||||
private static final String EXTENSION = ".java";
|
||||
private static final List<String> TARGET_ANNOTATIONS = List.of("EnableStateMachineFactory", "EnableStateMachine");
|
||||
public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package click.kamil.springstatemachineexporter.ast.app;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.Action;
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.Guard;
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.TransitionType;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AstTransitionParser {
|
||||
|
||||
private static final Set<String> SUPPORTED_TRANSITION_TYPES = Set.of(
|
||||
"withExternal", "withInternal", "withLocal", "withFork", "withJoin", "withChoice", "withJunction"
|
||||
);
|
||||
private static final Set<String> SUPPORTED_TRANSITION_METHOD_NAMES = Arrays.stream(TransitionType.values())
|
||||
.map(TransitionType::getMethodName)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
public static List<Transition> parseTransitions(MethodDeclaration method) {
|
||||
if (method == null) return Collections.emptyList();
|
||||
@@ -30,7 +34,7 @@ public class AstTransitionParser {
|
||||
return false;
|
||||
}
|
||||
List<MethodInvocation> chain = collectMethodInvocationChain(node);
|
||||
return chain.stream().anyMatch(mi -> SUPPORTED_TRANSITION_TYPES.contains(mi.getName().getIdentifier()));
|
||||
return chain.stream().anyMatch(mi -> SUPPORTED_TRANSITION_METHOD_NAMES.contains(mi.getName().getIdentifier()));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,18 +92,17 @@ public class AstTransitionParser {
|
||||
// Step 3: Parse each segment
|
||||
for (int i = 0; i < segments.size(); i++) {
|
||||
List<MethodInvocation> segment = segments.get(i);
|
||||
boolean isChoice = segment.stream().anyMatch(mi -> "withChoice".equals(mi.getName().getIdentifier()));
|
||||
boolean isJunction = segment.stream().anyMatch(mi -> "withJunction".equals(mi.getName().getIdentifier()));
|
||||
|
||||
Optional<TransitionType> segmentType = segment.stream()
|
||||
.map(mi -> TransitionType.fromMethodName(mi.getName().getIdentifier()))
|
||||
.flatMap(Optional::stream)
|
||||
.findFirst();
|
||||
|
||||
// If it's a continuation and we are at the first segment, we should be careful
|
||||
boolean isFirstSegmentOfContinuation = isContinuation && i == 0;
|
||||
|
||||
if (isChoice || isJunction) {
|
||||
List<Transition> choiceTransitions = parseChoiceOrJunctionTransitions(segment, isChoice ? "withChoice" : "withJunction", argsMap);
|
||||
if (isFirstSegmentOfContinuation) {
|
||||
// For choice continuations, the first transition often has an empty source (inherited from receiver)
|
||||
// We keep them as standard choice parsing handles it, but we ensure we don't introduce new start states
|
||||
}
|
||||
if (segmentType.isPresent() && (segmentType.get() == TransitionType.CHOICE || segmentType.get() == TransitionType.JUNCTION)) {
|
||||
List<Transition> choiceTransitions = parseChoiceOrJunctionTransitions(segment, segmentType.get(), argsMap);
|
||||
transitions.addAll(choiceTransitions);
|
||||
} else {
|
||||
Transition t = parseStandardTransition(segment, argsMap);
|
||||
@@ -118,7 +121,7 @@ public class AstTransitionParser {
|
||||
return transitions;
|
||||
}
|
||||
|
||||
private static List<Transition> parseChoiceOrJunctionTransitions(List<MethodInvocation> segment, String type, Map<String, Expression> argsMap) {
|
||||
private static List<Transition> parseChoiceOrJunctionTransitions(List<MethodInvocation> segment, TransitionType type, Map<String, Expression> argsMap) {
|
||||
List<Transition> transitions = new ArrayList<>();
|
||||
String sourceState = null;
|
||||
int orderCounter = 0;
|
||||
@@ -174,8 +177,9 @@ public class AstTransitionParser {
|
||||
String methodName = call.getName().getIdentifier();
|
||||
List<?> args = call.arguments();
|
||||
|
||||
if (SUPPORTED_TRANSITION_TYPES.contains(methodName)) {
|
||||
t.setType(methodName);
|
||||
Optional<TransitionType> type = TransitionType.fromMethodName(methodName);
|
||||
if (type.isPresent()) {
|
||||
t.setType(type.get());
|
||||
continue;
|
||||
}
|
||||
if (args.isEmpty()) {
|
||||
@@ -211,16 +215,14 @@ public class AstTransitionParser {
|
||||
private static void parseGuard(Object arg, Transition t) {
|
||||
QuotedExpression quotedExpr = QuotedExpression.of(arg);
|
||||
if (quotedExpr != null) {
|
||||
t.setGuard(quotedExpr.toStringWithoutQuotes());
|
||||
t.setLambdaGuard(isLambdaOrAnonymous(quotedExpr.getExpression()));
|
||||
t.setGuard(Guard.of(quotedExpr.toStringWithoutQuotes(), isLambdaOrAnonymous(quotedExpr.getExpression())));
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseAction(Object arg, Transition t) {
|
||||
QuotedExpression quotedExpr = QuotedExpression.of(arg);
|
||||
if (quotedExpr != null) {
|
||||
t.getActions().add(quotedExpr.toStringWithoutQuotes());
|
||||
t.getIsLambdaActions().add(isLambdaOrAnonymous(quotedExpr.getExpression()));
|
||||
t.getActions().add(Action.of(quotedExpr.toStringWithoutQuotes(), isLambdaOrAnonymous(quotedExpr.getExpression())));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package click.kamil.springstatemachineexporter.ast.app;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import org.eclipse.jdt.core.dom.StringLiteral;
|
||||
|
||||
@Getter
|
||||
public class QuotedExpression {
|
||||
@@ -13,6 +14,9 @@ public class QuotedExpression {
|
||||
|
||||
public String toStringWithoutQuotes() {
|
||||
if (expression == null) return null;
|
||||
if (expression instanceof StringLiteral sl) {
|
||||
return sl.getLiteralValue();
|
||||
}
|
||||
return stripQuotes(expression.toString());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package click.kamil.springstatemachineexporter.ast.app;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.TransitionType;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.Getter;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class StateMachineAggregator {
|
||||
private final CodebaseContext context;
|
||||
|
||||
private enum AnalysisGoal { TRANSITIONS, STATES }
|
||||
|
||||
public StateMachineAggregator(CodebaseContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
@@ -76,18 +80,35 @@ public class StateMachineAggregator {
|
||||
} else if (isTransitionChainContinuation(mi, argsMap)) {
|
||||
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), true));
|
||||
} else if (isForEach(mi)) {
|
||||
transitions.addAll(parseForEach(mi, searchStartClass, visitedMethods, argsMap, true, null, null));
|
||||
transitions.addAll(parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
||||
} else {
|
||||
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
|
||||
Expression receiver = mi.getExpression();
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
Expression lambdaReceiver = mi.getExpression();
|
||||
if (lambdaReceiver instanceof SimpleName sn) {
|
||||
Object resolved = argsMap.get(sn.getIdentifier());
|
||||
if (resolved instanceof LambdaExpression lambda) {
|
||||
transitions.addAll(parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, true, null, null));
|
||||
transitions.addAll(parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
||||
}
|
||||
}
|
||||
|
||||
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
||||
if (calledMethod == null) {
|
||||
// Try to resolve method in another class if it's a static call or on a known type
|
||||
Expression miReceiver = mi.getExpression();
|
||||
if (miReceiver instanceof SimpleName sn) {
|
||||
TypeDeclaration otherTd = context.getTypeDeclaration(sn.getIdentifier(), (CompilationUnit) searchStartClass.getRoot());
|
||||
if (otherTd != null) {
|
||||
calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), otherTd);
|
||||
if (calledMethod != null) {
|
||||
// For calls to other classes, we switch the "searchStartClass" to that class
|
||||
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
||||
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, otherTd, visitedMethods, nextArgsMap));
|
||||
return transitions; // Handled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (calledMethod != null) {
|
||||
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
||||
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, searchStartClass, visitedMethods, nextArgsMap));
|
||||
@@ -101,7 +122,7 @@ public class StateMachineAggregator {
|
||||
return mi.getName().getIdentifier().equals("forEach");
|
||||
}
|
||||
|
||||
private List<Transition> parseLambdaInvocation(LambdaExpression lambda, List<?> arguments, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, boolean isTransitions, Set<String> initialStates, Set<String> endStates) {
|
||||
private List<Transition> parseLambdaInvocation(LambdaExpression lambda, List<?> arguments, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
||||
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
|
||||
List<?> params = lambda.parameters();
|
||||
for (int i = 0; i < params.size() && i < arguments.size(); i++) {
|
||||
@@ -113,7 +134,7 @@ public class StateMachineAggregator {
|
||||
}
|
||||
}
|
||||
|
||||
if (isTransitions) {
|
||||
if (goal == AnalysisGoal.TRANSITIONS) {
|
||||
if (lambda.getBody() instanceof Block block) return parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap);
|
||||
else if (lambda.getBody() instanceof Expression lambdaExpr) return parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap);
|
||||
} else {
|
||||
@@ -123,7 +144,7 @@ public class StateMachineAggregator {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private List<Transition> parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, boolean isTransitions, Set<String> initialStates, Set<String> endStates) {
|
||||
private List<Transition> parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
||||
List<Transition> transitions = new ArrayList<>();
|
||||
Expression receiver = forEachMi.getExpression();
|
||||
List<Expression> elements = unrollCollection(receiver, argsMap);
|
||||
@@ -136,7 +157,7 @@ public class StateMachineAggregator {
|
||||
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
|
||||
if (!paramName.isEmpty()) lambdaArgsMap.put(paramName, element);
|
||||
|
||||
if (isTransitions) {
|
||||
if (goal == AnalysisGoal.TRANSITIONS) {
|
||||
if (lambda.getBody() instanceof Block block) transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap));
|
||||
else if (lambda.getBody() instanceof Expression lambdaExpr) transitions.addAll(parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap));
|
||||
} else {
|
||||
@@ -148,7 +169,7 @@ public class StateMachineAggregator {
|
||||
return transitions;
|
||||
}
|
||||
|
||||
private List<Transition> parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, boolean isTransitions, Set<String> initialStates, Set<String> endStates) {
|
||||
private List<Transition> parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
||||
List<Transition> transitions = new ArrayList<>();
|
||||
List<Expression> elements = unrollCollection(efs.getExpression(), argsMap);
|
||||
String paramName = efs.getParameter().getName().getIdentifier();
|
||||
@@ -157,7 +178,7 @@ public class StateMachineAggregator {
|
||||
Map<String, Object> loopArgsMap = new HashMap<>(argsMap);
|
||||
loopArgsMap.put(paramName, element);
|
||||
|
||||
if (isTransitions) {
|
||||
if (goal == AnalysisGoal.TRANSITIONS) {
|
||||
if (efs.getBody() instanceof Block block) transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, loopArgsMap));
|
||||
else if (efs.getBody() instanceof ExpressionStatement es) transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, loopArgsMap));
|
||||
} else {
|
||||
@@ -217,7 +238,7 @@ public class StateMachineAggregator {
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (stmt instanceof EnhancedForStatement efs) transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, true, null, null));
|
||||
else if (stmt instanceof EnhancedForStatement efs) transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
||||
else if (stmt instanceof TryStatement ts) transitions.addAll(parseTransitionsInBlock(ts.getBody(), searchStartClass, visitedMethods, argsMap));
|
||||
}
|
||||
return transitions;
|
||||
@@ -238,7 +259,7 @@ public class StateMachineAggregator {
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (stmt instanceof EnhancedForStatement efs) parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, false, initialStates, endStates);
|
||||
else if (stmt instanceof EnhancedForStatement efs) parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
||||
else if (stmt instanceof TryStatement ts) parseStatesInBlock(ts.getBody(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||
}
|
||||
}
|
||||
@@ -246,14 +267,14 @@ public class StateMachineAggregator {
|
||||
private void parseStatesInExpression(Expression expr, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||
if (expr instanceof MethodInvocation mi) {
|
||||
if (isWithStatesChain(mi, argsMap)) parseStateChain(mi, initialStates, endStates, argsMap);
|
||||
else if (isForEach(mi)) parseForEach(mi, searchStartClass, visitedMethods, argsMap, false, initialStates, endStates);
|
||||
else if (isForEach(mi)) parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
||||
else {
|
||||
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
|
||||
Expression receiver = mi.getExpression();
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
Object resolved = argsMap.get(sn.getIdentifier());
|
||||
if (resolved instanceof LambdaExpression lambda) {
|
||||
parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, false, initialStates, endStates);
|
||||
parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +343,7 @@ public class StateMachineAggregator {
|
||||
|
||||
private boolean isTransitionChainEntry(MethodInvocation mi) {
|
||||
String name = mi.getName().getIdentifier();
|
||||
if (Set.of("withExternal", "withInternal", "withLocal", "withFork", "withJoin", "withChoice", "withJunction").contains(name)) return true;
|
||||
if (TransitionType.fromMethodName(name).isPresent()) return true;
|
||||
Expression receiver = mi.getExpression();
|
||||
if (receiver instanceof MethodInvocation next) return isTransitionChainEntry(next);
|
||||
return false;
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
package click.kamil.springstatemachineexporter.ast.app;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class StateMachineStateConfigurationMethodFinder {
|
||||
|
||||
public static CompilationUnit parse(String source) {
|
||||
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
||||
parser.setSource(source.toCharArray());
|
||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
||||
parser.setResolveBindings(false);
|
||||
return (CompilationUnit) parser.createAST(null);
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class StateConfigurerVisitor extends ASTVisitor {
|
||||
|
||||
private final Set<String> initialStates = new HashSet<>();
|
||||
private final Set<String> endStates = new HashSet<>();
|
||||
|
||||
private boolean visitConfigureMethod(MethodDeclaration node) {
|
||||
if (!"configure".equals(node.getName().getIdentifier())) {
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
Block body = node.getBody();
|
||||
if (body == null) return false;
|
||||
|
||||
// Traverse statements inside configure method
|
||||
for (Object stmtObj : body.statements()) {
|
||||
if (!(stmtObj instanceof ExpressionStatement exprStmt)) continue;
|
||||
Expression expr = exprStmt.getExpression();
|
||||
if (expr instanceof MethodInvocation rootCall) {
|
||||
|
||||
// Check if this chain starts with "withStates"
|
||||
if (isWithStatesChain(rootCall)) {
|
||||
parseMethodChain(rootCall);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false; // no need to visit further inside configure
|
||||
}
|
||||
private boolean visitBuilderMethod(MethodDeclaration node) {
|
||||
Block body = node.getBody();
|
||||
if (body == null) return false;
|
||||
|
||||
// Traverse statements inside buildStateMachine method
|
||||
for (Object stmtObj : body.statements()) {
|
||||
if (!(stmtObj instanceof ExpressionStatement exprStmt)) continue;
|
||||
Expression expr = exprStmt.getExpression();
|
||||
if (expr instanceof MethodInvocation rootCall) {
|
||||
// Detect when the chain starts with "configureStates"
|
||||
// Start looping through the method chain
|
||||
findConfigureStates(rootCall);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private void findConfigureStates(MethodInvocation methodInvocation) {
|
||||
// Start at the outermost method invocation
|
||||
MethodInvocation currentInvocation = methodInvocation;
|
||||
|
||||
while (currentInvocation != null) {
|
||||
// Check if the current method in the chain is 'configureStates'
|
||||
if ("configureStates".equals(currentInvocation.getName().getIdentifier())) {
|
||||
// Once found, call the parsing method
|
||||
parseStateMachineMethodChain(methodInvocation);
|
||||
return; // Exit once we've handled the method
|
||||
}
|
||||
|
||||
// Move to the next method invocation in the chain (moving forward)
|
||||
Expression nextExpression = currentInvocation.getExpression();
|
||||
|
||||
// Check if the next expression is another method invocation
|
||||
if (nextExpression instanceof MethodInvocation) {
|
||||
// Continue to the next method invocation
|
||||
currentInvocation = (MethodInvocation) nextExpression;
|
||||
} else {
|
||||
// No more method calls in the chain, break out of the loop
|
||||
currentInvocation = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for method chain starting with "configureStates"
|
||||
private void parseStateMachineMethodChain(MethodInvocation mi) {
|
||||
List<MethodInvocation> chain = new ArrayList<>();
|
||||
Expression current = mi;
|
||||
while (current instanceof MethodInvocation m) {
|
||||
chain.addFirst(m);
|
||||
current = m.getExpression();
|
||||
}
|
||||
|
||||
for (MethodInvocation call : chain) {
|
||||
String methodName = call.getName().getIdentifier();
|
||||
List<?> args = call.arguments();
|
||||
if (args.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
Expression arg = (Expression) args.get(0);
|
||||
switch (methodName) {
|
||||
case "initial" -> {
|
||||
if (!isInsideRegionOrFork(call)) {
|
||||
initialStates.add(QuotedExpression.of(arg).toStringWithoutQuotes());
|
||||
}
|
||||
}
|
||||
case "end" -> {
|
||||
endStates.add(QuotedExpression.of(arg).toStringWithoutQuotes());
|
||||
}
|
||||
// You can also add cases for other methods if needed (e.g., stateEntry, stateExit)
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(MethodDeclaration node) {
|
||||
// Visit method named "configure" only
|
||||
if ("configure".equals(node.getName().getIdentifier())) {
|
||||
return visitConfigureMethod(node);
|
||||
}
|
||||
// Handle builder method configuration (e.g., for buildStateMachine)
|
||||
if ("buildStateMachine".equals(node.getName().getIdentifier())) {
|
||||
return visitBuilderMethod(node);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private boolean isWithStatesChain(MethodInvocation mi) {
|
||||
MethodInvocation current = mi;
|
||||
while (true) {
|
||||
Expression expr = current.getExpression();
|
||||
if ("withStates".equals(current.getName().getIdentifier())) {
|
||||
return true;
|
||||
}
|
||||
if (expr instanceof MethodInvocation next) {
|
||||
current = next;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInsideRegionOrFork(MethodInvocation mi) {
|
||||
Expression expr = mi.getExpression();
|
||||
while (expr instanceof MethodInvocation parent) {
|
||||
String methodName = parent.getName().getIdentifier();
|
||||
if (List.of("fork", "region").contains(methodName)) {
|
||||
return true;
|
||||
}
|
||||
expr = parent.getExpression();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void parseMethodChain(MethodInvocation mi) {
|
||||
List<MethodInvocation> chain = new ArrayList<>();
|
||||
Expression current = mi;
|
||||
while (current instanceof MethodInvocation m) {
|
||||
chain.addFirst(m);
|
||||
current = m.getExpression();
|
||||
}
|
||||
|
||||
for (MethodInvocation call : chain) {
|
||||
String methodName = call.getName().getIdentifier();
|
||||
List<?> args = call.arguments();
|
||||
if (args.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
Expression arg = (Expression) args.getFirst();
|
||||
switch (methodName) {
|
||||
case "initial" -> {
|
||||
if (!isInsideRegionOrFork(call)) {
|
||||
initialStates.add(QuotedExpression.of(arg).toStringWithoutQuotes());
|
||||
}
|
||||
}
|
||||
case "end" -> endStates.add(QuotedExpression.of(arg).toStringWithoutQuotes());
|
||||
// ignore others
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package click.kamil.springstatemachineexporter.ast.app;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
||||
import lombok.Getter;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class StateMachineTransitionConfigurationMethodFinder {
|
||||
private final CompilationUnit cu;
|
||||
private final ConfigureMethodVisitor visitor = new ConfigureMethodVisitor();
|
||||
private static final List<String> stateMachineConfigurationAdapterSuperclasses = List.of("EnumStateMachineConfigurerAdapter", "StateMachineConfigurerAdapter");
|
||||
|
||||
public StateMachineTransitionConfigurationMethodFinder(String source) {
|
||||
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
||||
parser.setSource(source.toCharArray());
|
||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
||||
parser.setResolveBindings(false);
|
||||
|
||||
this.cu = (CompilationUnit) parser.createAST(null);
|
||||
}
|
||||
|
||||
public MethodDeclaration findConfigureMethod() {
|
||||
this.cu.accept(visitor);
|
||||
return visitor.getConfigureMethod();
|
||||
}
|
||||
|
||||
public Set<MethodDeclaration> findMethodsWithReturnType(Set<String> returnTypes) {
|
||||
Set<MethodDeclaration> matchedMethods = new HashSet<>();
|
||||
cu.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodDeclaration node) {
|
||||
Type returnType = node.getReturnType2();
|
||||
String typeName = (returnType == null) ? "void" : AstUtils.extractSimpleTypeName(returnType);
|
||||
if (returnTypes.contains(typeName)) {
|
||||
matchedMethods.add(node);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return matchedMethods;
|
||||
}
|
||||
|
||||
|
||||
@Getter
|
||||
private static class ConfigureMethodVisitor extends ASTVisitor {
|
||||
private MethodDeclaration configureMethod = null;
|
||||
|
||||
@Override
|
||||
public boolean visit(TypeDeclaration node) {
|
||||
if (!node.isInterface() && extendsStateMachineConfigurerAdapter(node)) {
|
||||
for (MethodDeclaration method : node.getMethods()) {
|
||||
if (isConfigureMethod(method)) {
|
||||
configureMethod = method;
|
||||
return false; // Found, stop visiting further
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean extendsStateMachineConfigurerAdapter(TypeDeclaration node) {
|
||||
Type superclass = node.getSuperclassType();
|
||||
if (superclass == null)
|
||||
return false;
|
||||
|
||||
if (superclass.isParameterizedType()) {
|
||||
ParameterizedType pt = (ParameterizedType) superclass;
|
||||
return stateMachineConfigurationAdapterSuperclasses.contains(pt.getType().toString());
|
||||
}
|
||||
return stateMachineConfigurationAdapterSuperclasses.contains(superclass.toString());
|
||||
}
|
||||
|
||||
private boolean isConfigureMethod(MethodDeclaration method) {
|
||||
if (!method.getName().getIdentifier().equals("configure")) return false;
|
||||
|
||||
if (!Modifier.isPublic(method.getModifiers())) return false;
|
||||
Type returnType = method.getReturnType2();
|
||||
if (returnType == null || !returnType.isPrimitiveType() || !"void".equals(returnType.toString()))
|
||||
return false;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<SingleVariableDeclaration> params = method.parameters();
|
||||
if (params.size() != 1)
|
||||
return false;
|
||||
|
||||
if (!params.getFirst().getType().toString().startsWith("StateMachineTransitionConfigurer"))
|
||||
return false;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Type> thrownExceptions = method.thrownExceptionTypes();
|
||||
for (Type excType : thrownExceptions) {
|
||||
if (excType.toString().equals("Exception")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package click.kamil.springstatemachineexporter.ast.app.domain;
|
||||
|
||||
public record Action(String expression, boolean isLambda) {
|
||||
public static Action of(String expression, boolean isLambda) {
|
||||
return expression != null ? new Action(expression, isLambda) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package click.kamil.springstatemachineexporter.ast.app.domain;
|
||||
|
||||
public record Guard(String expression, boolean isLambda) {
|
||||
public static Guard of(String expression, boolean isLambda) {
|
||||
return expression != null ? new Guard(expression, isLambda) : null;
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,13 @@ import java.util.List;
|
||||
@Getter
|
||||
@Setter
|
||||
public class Transition {
|
||||
private String type; // withExternal, withChoice, withInternal, etc.
|
||||
private TransitionType type;
|
||||
private List<String> sourceStates = new ArrayList<>();
|
||||
private List<String> targetStates = new ArrayList<>();
|
||||
private String event;
|
||||
|
||||
private String guard;
|
||||
private boolean isLambdaGuard = false;
|
||||
|
||||
private List<String> actions = new ArrayList<>();
|
||||
private List<Boolean> isLambdaActions = new ArrayList<>();
|
||||
private Guard guard;
|
||||
private List<Action> actions = new ArrayList<>();
|
||||
|
||||
private Integer order = null; // optional order for withChoice or withJunction
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package click.kamil.springstatemachineexporter.ast.app.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum TransitionType {
|
||||
EXTERNAL("withExternal"),
|
||||
INTERNAL("withInternal"),
|
||||
LOCAL("withLocal"),
|
||||
FORK("withFork"),
|
||||
JOIN("withJoin"),
|
||||
CHOICE("withChoice"),
|
||||
JUNCTION("withJunction");
|
||||
|
||||
private final String methodName;
|
||||
|
||||
public static Optional<TransitionType> fromMethodName(String methodName) {
|
||||
return Arrays.stream(values())
|
||||
.filter(t -> t.methodName.equals(methodName))
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package click.kamil.springstatemachineexporter.ast.in;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class AstFileFinder {
|
||||
private final ASTParser parser;
|
||||
|
||||
public AstFileFinder() {
|
||||
this.parser = ASTParser.newParser(AST.getJLSLatest());
|
||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
||||
parser.setResolveBindings(false);
|
||||
}
|
||||
|
||||
public boolean hasClassWithAnnotation(String source, List<String> annotationNames) {
|
||||
parser.setSource(source.toCharArray());
|
||||
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
|
||||
AtomicBoolean found = new AtomicBoolean(false);
|
||||
cu.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(TypeDeclaration node) {
|
||||
// skip interfaces
|
||||
if (node.isInterface()) return true;
|
||||
for (Object modifier : node.modifiers()) {
|
||||
if (modifier instanceof Annotation annotation &&
|
||||
annotationNames.contains(annotation.getTypeName().getFullyQualifiedName())) {
|
||||
found.set(true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return found.get();
|
||||
}
|
||||
public boolean hasFunctionReturningType(String source, Set<String> targetReturnTypes) {
|
||||
parser.setSource(source.toCharArray());
|
||||
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
|
||||
AtomicBoolean found = new AtomicBoolean(false);
|
||||
|
||||
cu.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodDeclaration node) {
|
||||
Type returnType = node.getReturnType2();
|
||||
String typeName = (returnType == null) ? "void" : AstUtils.extractSimpleTypeName(returnType);
|
||||
|
||||
if (targetReturnTypes.contains(typeName)) {
|
||||
// Also check for @Bean annotation
|
||||
boolean hasBeanAnnotation = false;
|
||||
for (Object modifier : node.modifiers()) {
|
||||
if (modifier instanceof Annotation annotation) {
|
||||
String annotationName = annotation.getTypeName().getFullyQualifiedName();
|
||||
if ("Bean".equals(annotationName) || "org.springframework.context.annotation.Bean".equals(annotationName)) {
|
||||
hasBeanAnnotation = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasBeanAnnotation) {
|
||||
found.set(true);
|
||||
return false; // stop visiting
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return found.get();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package click.kamil.springstatemachineexporter.ast.out;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.TransitionType;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -29,7 +30,7 @@ public class Dot implements StateMachineExporter {
|
||||
int colorIndex = 0;
|
||||
|
||||
for (Transition t : transitions) {
|
||||
if ("withChoice".equals(t.getType()) && t.getSourceStates() != null) {
|
||||
if (t.getType() == TransitionType.CHOICE && t.getSourceStates() != null) {
|
||||
for (String source : t.getSourceStates()) {
|
||||
String simplified = simplify(source);
|
||||
statesWithChoice.add(simplified);
|
||||
@@ -90,7 +91,7 @@ public class Dot implements StateMachineExporter {
|
||||
for (Transition t : transitions) {
|
||||
if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue;
|
||||
|
||||
boolean allowNoTarget = "withJoin".equals(t.getType()) || "withFork".equals(t.getType());
|
||||
boolean allowNoTarget = t.getType() == TransitionType.JOIN || t.getType() == TransitionType.FORK;
|
||||
List<String> targets = t.getTargetStates();
|
||||
|
||||
if (targets == null || targets.isEmpty()) {
|
||||
@@ -108,7 +109,7 @@ public class Dot implements StateMachineExporter {
|
||||
String target = simplify(rawTarget);
|
||||
String edgeSource = source;
|
||||
|
||||
if ("withChoice".equals(t.getType()) && statesWithChoice.contains(source)) {
|
||||
if (t.getType() == TransitionType.CHOICE && statesWithChoice.contains(source)) {
|
||||
edgeSource = includeChoiceStates ? source + "_choice" : source;
|
||||
}
|
||||
|
||||
@@ -120,10 +121,10 @@ public class Dot implements StateMachineExporter {
|
||||
}
|
||||
|
||||
// Guard
|
||||
if (t.getGuard() != null && !t.getGuard().isBlank()) {
|
||||
if (t.getGuard() != null && !t.getGuard().expression().isBlank()) {
|
||||
if (!label.isEmpty()) label.append(" ");
|
||||
String guardText = useLambdaGuards && t.isLambdaGuard() ? "λ"
|
||||
: escapeDotString(t.getGuard().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim());
|
||||
String guardText = useLambdaGuards && t.getGuard().isLambda() ? "λ"
|
||||
: escapeDotString(t.getGuard().expression().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim());
|
||||
label.append("[").append(guardText).append("]");
|
||||
}
|
||||
|
||||
@@ -132,16 +133,15 @@ public class Dot implements StateMachineExporter {
|
||||
StringBuilder actionBuilder = new StringBuilder();
|
||||
for (int i = 0; i < t.getActions().size(); i++) {
|
||||
if (i > 0) actionBuilder.append(", ");
|
||||
String action = t.getActions().get(i);
|
||||
boolean isLambda = i < t.getIsLambdaActions().size() && t.getIsLambdaActions().get(i);
|
||||
actionBuilder.append(useLambdaGuards && isLambda ? "λ" : escapeDotString(action));
|
||||
var action = t.getActions().get(i);
|
||||
actionBuilder.append(useLambdaGuards && action.isLambda() ? "λ" : escapeDotString(action.expression()));
|
||||
}
|
||||
if (!label.isEmpty()) label.append(" / ");
|
||||
label.append(actionBuilder);
|
||||
}
|
||||
|
||||
// Order
|
||||
if ((t.getType().equals("withChoice") || t.getType().equals("withJunction")) && t.getOrder() != null) {
|
||||
if ((t.getType() == TransitionType.CHOICE || t.getType() == TransitionType.JUNCTION) && t.getOrder() != null) {
|
||||
if (!label.isEmpty()) label.append(" ");
|
||||
label.append("(order=").append(t.getOrder()).append(")");
|
||||
}
|
||||
@@ -153,36 +153,41 @@ public class Dot implements StateMachineExporter {
|
||||
}
|
||||
|
||||
// Style/color by type
|
||||
switch (t.getType()) {
|
||||
case "withInternal":
|
||||
appendEdgeAttr(edgeAttrs, "style", "dashed");
|
||||
appendEdgeAttr(edgeAttrs, "color", "gray");
|
||||
break;
|
||||
case "withLocal":
|
||||
appendEdgeAttr(edgeAttrs, "style", "dotted");
|
||||
appendEdgeAttr(edgeAttrs, "color", "darkgray");
|
||||
break;
|
||||
case "withChoice":
|
||||
String choiceColor = choiceColorMap.getOrDefault(source, "blue");
|
||||
appendEdgeAttr(edgeAttrs, "style", "solid");
|
||||
appendEdgeAttr(edgeAttrs, "color", choiceColor);
|
||||
break;
|
||||
case "withJunction":
|
||||
appendEdgeAttr(edgeAttrs, "style", "dotted");
|
||||
appendEdgeAttr(edgeAttrs, "color", "purple");
|
||||
break;
|
||||
case "withFork":
|
||||
appendEdgeAttr(edgeAttrs, "style", "bold");
|
||||
appendEdgeAttr(edgeAttrs, "color", "green");
|
||||
break;
|
||||
case "withJoin":
|
||||
appendEdgeAttr(edgeAttrs, "style", "bold");
|
||||
appendEdgeAttr(edgeAttrs, "color", "darkorange");
|
||||
break;
|
||||
case "withExternal":
|
||||
default:
|
||||
appendEdgeAttr(edgeAttrs, "style", "solid");
|
||||
appendEdgeAttr(edgeAttrs, "color", "black");
|
||||
if (t.getType() != null) {
|
||||
switch (t.getType()) {
|
||||
case INTERNAL:
|
||||
appendEdgeAttr(edgeAttrs, "style", "dashed");
|
||||
appendEdgeAttr(edgeAttrs, "color", "gray");
|
||||
break;
|
||||
case LOCAL:
|
||||
appendEdgeAttr(edgeAttrs, "style", "dotted");
|
||||
appendEdgeAttr(edgeAttrs, "color", "darkgray");
|
||||
break;
|
||||
case CHOICE:
|
||||
String choiceColor = choiceColorMap.getOrDefault(source, "blue");
|
||||
appendEdgeAttr(edgeAttrs, "style", "solid");
|
||||
appendEdgeAttr(edgeAttrs, "color", choiceColor);
|
||||
break;
|
||||
case JUNCTION:
|
||||
appendEdgeAttr(edgeAttrs, "style", "dotted");
|
||||
appendEdgeAttr(edgeAttrs, "color", "purple");
|
||||
break;
|
||||
case FORK:
|
||||
appendEdgeAttr(edgeAttrs, "style", "bold");
|
||||
appendEdgeAttr(edgeAttrs, "color", "green");
|
||||
break;
|
||||
case JOIN:
|
||||
appendEdgeAttr(edgeAttrs, "style", "bold");
|
||||
appendEdgeAttr(edgeAttrs, "color", "darkorange");
|
||||
break;
|
||||
case EXTERNAL:
|
||||
default:
|
||||
appendEdgeAttr(edgeAttrs, "style", "solid");
|
||||
appendEdgeAttr(edgeAttrs, "color", "black");
|
||||
}
|
||||
} else {
|
||||
appendEdgeAttr(edgeAttrs, "style", "solid");
|
||||
appendEdgeAttr(edgeAttrs, "color", "black");
|
||||
}
|
||||
|
||||
sb.append(" ").append(edgeSource).append(" -> ").append(target);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package click.kamil.springstatemachineexporter.ast.out;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.TransitionType;
|
||||
import click.kamil.springstatemachineexporter.ast.common.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
@@ -11,15 +12,15 @@ public class PlantUml implements StateMachineExporter {
|
||||
// Color palette for choices
|
||||
private final List<String> choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4");
|
||||
private String getColor(Transition t, String source, Map<String, String> choiceStateColorMap) {
|
||||
if (t.getType() == null) return "000000";
|
||||
return switch (t.getType()) {
|
||||
case "withChoice" -> choiceStateColorMap.getOrDefault(source, "000000");
|
||||
case "withExternal" -> "1E90FF";
|
||||
case "withInternal" -> "32CD32";
|
||||
case "withLocal" -> "FFA500";
|
||||
case "withJunction" -> "FF69B4";
|
||||
case "withJoin" -> "8A2BE2";
|
||||
case "withFork" -> "20B2AA";
|
||||
default -> "000000";
|
||||
case CHOICE -> choiceStateColorMap.getOrDefault(source, "000000");
|
||||
case EXTERNAL -> "1E90FF";
|
||||
case INTERNAL -> "32CD32";
|
||||
case LOCAL -> "FFA500";
|
||||
case JUNCTION -> "FF69B4";
|
||||
case JOIN -> "8A2BE2";
|
||||
case FORK -> "20B2AA";
|
||||
};
|
||||
}
|
||||
private final String LAMBDA = "λ";
|
||||
@@ -45,7 +46,7 @@ public class PlantUml implements StateMachineExporter {
|
||||
// 2. Find all states that have choice transitions originating from them
|
||||
Set<String> statesWithChoice = new HashSet<>();
|
||||
for (Transition t : transitions) {
|
||||
if ("withChoice".equals(t.getType())) {
|
||||
if (t.getType() == TransitionType.CHOICE) {
|
||||
for (String source : t.getSourceStates()) {
|
||||
statesWithChoice.add(simplify(source));
|
||||
}
|
||||
@@ -79,7 +80,7 @@ public class PlantUml implements StateMachineExporter {
|
||||
for (Transition t : transitions) {
|
||||
if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue;
|
||||
|
||||
boolean allowNoTarget = "withJoin".equals(t.getType()) || "withFork".equals(t.getType());
|
||||
boolean allowNoTarget = t.getType() == TransitionType.JOIN || t.getType() == TransitionType.FORK;
|
||||
|
||||
List<String> targets;
|
||||
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||
@@ -99,7 +100,7 @@ public class PlantUml implements StateMachineExporter {
|
||||
String target = simplify(rawTarget);
|
||||
String transitionSource;
|
||||
if (includeChoiceStates) {
|
||||
transitionSource = "withChoice".equals(t.getType()) && statesWithChoice.contains(source)
|
||||
transitionSource = t.getType() == TransitionType.CHOICE && statesWithChoice.contains(source)
|
||||
? source + "_choice"
|
||||
: source;
|
||||
} else {
|
||||
@@ -134,14 +135,13 @@ public class PlantUml implements StateMachineExporter {
|
||||
}
|
||||
|
||||
private String getGuardText(Transition t, boolean useLambdaGuards) {
|
||||
String guard = t.getGuard();
|
||||
if (StringUtils.isBlank(guard)) {
|
||||
if (t.getGuard() == null || StringUtils.isBlank(t.getGuard().expression())) {
|
||||
return "";
|
||||
}
|
||||
if (useLambdaGuards && t.isLambdaGuard()) {
|
||||
if (useLambdaGuards && t.getGuard().isLambda()) {
|
||||
return LAMBDA;
|
||||
}
|
||||
return guard
|
||||
return t.getGuard().expression()
|
||||
.replaceAll("[\\n\\r]+", " ")
|
||||
.replaceAll("\\s{2,}", " ")
|
||||
.trim();
|
||||
@@ -152,14 +152,8 @@ public class PlantUml implements StateMachineExporter {
|
||||
return "";
|
||||
}
|
||||
|
||||
List<String> actions = t.getActions();
|
||||
List<Boolean> isLambdaFlags = t.getIsLambdaActions();
|
||||
|
||||
// Defensive: ensure lists are same size to avoid IndexOutOfBoundsException
|
||||
int size = Math.min(actions.size(), isLambdaFlags.size());
|
||||
|
||||
return IntStream.range(0, size)
|
||||
.mapToObj(i -> (useLambdaGuards && isLambdaFlags.get(i)) ? LAMBDA : actions.get(i))
|
||||
return t.getActions().stream()
|
||||
.map(action -> (useLambdaGuards && action.isLambda()) ? LAMBDA : action.expression())
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
@@ -185,7 +179,7 @@ public class PlantUml implements StateMachineExporter {
|
||||
}
|
||||
});
|
||||
Optional.ofNullable(t.getOrder())
|
||||
.filter(order -> List.of("withChoice", "withJunction").contains(t.getType()))
|
||||
.filter(order -> t.getType() == TransitionType.CHOICE || t.getType() == TransitionType.JUNCTION)
|
||||
.map(order -> "(order=" + order + ")")
|
||||
.ifPresent(parts::add);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package click.kamil.springstatemachineexporter.ast.out;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.TransitionType;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -40,7 +41,7 @@ public class Scxml implements StateMachineExporter {
|
||||
for (Transition t : transitions) {
|
||||
if (t.getSourceStates() == null || !t.getSourceStates().contains(state)) continue;
|
||||
|
||||
boolean allowNoTarget = "withJoin".equals(t.getType()) || "withFork".equals(t.getType());
|
||||
boolean allowNoTarget = t.getType() == TransitionType.JOIN || t.getType() == TransitionType.FORK;
|
||||
List<String> targets;
|
||||
|
||||
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||
@@ -74,11 +75,11 @@ public class Scxml implements StateMachineExporter {
|
||||
}
|
||||
|
||||
private static String getGuardText(Transition t, boolean useLambdaGuards) {
|
||||
if (t.getGuard() == null || t.getGuard().isBlank()) return "";
|
||||
if (useLambdaGuards && t.isLambdaGuard()) {
|
||||
if (t.getGuard() == null || t.getGuard().expression().isBlank()) return "";
|
||||
if (useLambdaGuards && t.getGuard().isLambda()) {
|
||||
return LAMBDA;
|
||||
}
|
||||
return t.getGuard().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
|
||||
return t.getGuard().expression().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
|
||||
}
|
||||
|
||||
private static String escapeXml(String s) {
|
||||
@@ -102,7 +103,7 @@ public class Scxml implements StateMachineExporter {
|
||||
}
|
||||
|
||||
// Choice/junction order → XML comment
|
||||
if (("withChoice".equals(t.getType()) || "withJunction".equals(t.getType())) && t.getOrder() != null) {
|
||||
if ((t.getType() == TransitionType.CHOICE || t.getType() == TransitionType.JUNCTION) && t.getOrder() != null) {
|
||||
sb.append(">\n");
|
||||
sb.append(" <!-- order=").append(t.getOrder()).append(" -->\n");
|
||||
sb.append(" </transition>\n");
|
||||
|
||||
Reference in New Issue
Block a user