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");
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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 org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -27,7 +30,7 @@ class AstTransitionParserTest {
|
||||
Transition transition = transitions.getFirst();
|
||||
assertThat(transition)
|
||||
.extracting(Transition::getType)
|
||||
.isEqualTo("withExternal");
|
||||
.isEqualTo(TransitionType.EXTERNAL);
|
||||
assertThat(transition.getSourceStates()).containsExactly("CREATED");
|
||||
assertThat(transition.getTargetStates()).containsExactly("PAID");
|
||||
assertThat(transition)
|
||||
@@ -50,10 +53,12 @@ class AstTransitionParserTest {
|
||||
|
||||
assertThat(transitions).hasSize(1);
|
||||
Transition transition = transitions.getFirst();
|
||||
assertThat(transition)
|
||||
.extracting(Transition::getGuard)
|
||||
assertThat(transition.getGuard())
|
||||
.extracting(Guard::expression)
|
||||
.isEqualTo("amount > 0");
|
||||
assertThat(transition.isLambdaGuard()).isFalse();
|
||||
assertThat(transition.getGuard())
|
||||
.extracting(Guard::isLambda)
|
||||
.isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -71,8 +76,8 @@ class AstTransitionParserTest {
|
||||
|
||||
assertThat(transitions).hasSize(1);
|
||||
Transition transition = transitions.getFirst();
|
||||
assertThat(transition.getGuard()).contains("context -> context.getExtendedState().getVariables().get(\"amount\") > 0");
|
||||
assertThat(transition.isLambdaGuard()).isTrue();
|
||||
assertThat(transition.getGuard().expression()).contains("context -> context.getExtendedState().getVariables().get(\"amount\") > 0");
|
||||
assertThat(transition.getGuard().isLambda()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -90,8 +95,12 @@ class AstTransitionParserTest {
|
||||
|
||||
assertThat(transitions).hasSize(1);
|
||||
Transition transition = transitions.getFirst();
|
||||
assertThat(transition.getActions()).containsExactly("processPayment()");
|
||||
assertThat(transition.getIsLambdaActions()).containsExactly(false);
|
||||
assertThat(transition.getActions())
|
||||
.extracting(Action::expression)
|
||||
.containsExactly("processPayment()");
|
||||
assertThat(transition.getActions())
|
||||
.extracting(Action::isLambda)
|
||||
.containsExactly(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,9 +118,12 @@ class AstTransitionParserTest {
|
||||
|
||||
assertThat(transitions).hasSize(1);
|
||||
Transition transition = transitions.getFirst();
|
||||
assertThat(transition.getActions()).hasSize(1);
|
||||
assertThat(transition.getActions().getFirst()).contains("context -> System.out.println(\"Payment processed\")");
|
||||
assertThat(transition.getIsLambdaActions()).containsExactly(true);
|
||||
assertThat(transition.getActions())
|
||||
.extracting(Action::expression)
|
||||
.anyMatch(s -> s.contains("context -> System.out.println(\"Payment processed\")"));
|
||||
assertThat(transition.getActions())
|
||||
.extracting(Action::isLambda)
|
||||
.containsExactly(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -132,7 +144,7 @@ class AstTransitionParserTest {
|
||||
Transition first = transitions.getFirst();
|
||||
assertThat(first)
|
||||
.extracting(Transition::getType)
|
||||
.isEqualTo("withExternal");
|
||||
.isEqualTo(TransitionType.EXTERNAL);
|
||||
assertThat(first.getSourceStates()).containsExactly("CREATED");
|
||||
assertThat(first.getTargetStates()).containsExactly("PAID");
|
||||
assertThat(first)
|
||||
@@ -142,7 +154,7 @@ class AstTransitionParserTest {
|
||||
Transition second = transitions.get(1);
|
||||
assertThat(second)
|
||||
.extracting(Transition::getType)
|
||||
.isEqualTo("withExternal");
|
||||
.isEqualTo(TransitionType.EXTERNAL);
|
||||
assertThat(second.getSourceStates()).containsExactly("PAID");
|
||||
assertThat(second.getTargetStates()).containsExactly("SHIPPED");
|
||||
assertThat(second)
|
||||
@@ -168,11 +180,11 @@ class AstTransitionParserTest {
|
||||
Transition first = transitions.getFirst();
|
||||
assertThat(first)
|
||||
.extracting(Transition::getType)
|
||||
.isEqualTo("withChoice");
|
||||
.isEqualTo(TransitionType.CHOICE);
|
||||
assertThat(first.getSourceStates()).containsExactly("PAID");
|
||||
assertThat(first.getTargetStates()).containsExactly("SHIPPED");
|
||||
assertThat(first)
|
||||
.extracting(Transition::getGuard)
|
||||
assertThat(first.getGuard())
|
||||
.extracting(Guard::expression)
|
||||
.isEqualTo("isExpress()");
|
||||
assertThat(first)
|
||||
.extracting(Transition::getOrder)
|
||||
@@ -181,11 +193,11 @@ class AstTransitionParserTest {
|
||||
Transition second = transitions.get(1);
|
||||
assertThat(second)
|
||||
.extracting(Transition::getType)
|
||||
.isEqualTo("withChoice");
|
||||
.isEqualTo(TransitionType.CHOICE);
|
||||
assertThat(second.getSourceStates()).containsExactly("PAID");
|
||||
assertThat(second.getTargetStates()).containsExactly("CANCELLED");
|
||||
assertThat(second)
|
||||
.extracting(Transition::getGuard)
|
||||
assertThat(second.getGuard())
|
||||
.extracting(Guard::expression)
|
||||
.isEqualTo("isCancelled()");
|
||||
assertThat(second)
|
||||
.extracting(Transition::getOrder)
|
||||
@@ -194,7 +206,7 @@ class AstTransitionParserTest {
|
||||
Transition third = transitions.get(2);
|
||||
assertThat(third)
|
||||
.extracting(Transition::getType)
|
||||
.isEqualTo("withChoice");
|
||||
.isEqualTo(TransitionType.CHOICE);
|
||||
assertThat(third.getSourceStates()).containsExactly("PAID");
|
||||
assertThat(third.getTargetStates()).containsExactly("PENDING");
|
||||
assertThat(third)
|
||||
@@ -218,13 +230,13 @@ class AstTransitionParserTest {
|
||||
assertThat(transitions).hasSize(3);
|
||||
assertThat(transitions.getFirst())
|
||||
.extracting(Transition::getType)
|
||||
.isEqualTo("withJunction");
|
||||
.isEqualTo(TransitionType.JUNCTION);
|
||||
assertThat(transitions.get(1))
|
||||
.extracting(Transition::getType)
|
||||
.isEqualTo("withJunction");
|
||||
.isEqualTo(TransitionType.JUNCTION);
|
||||
assertThat(transitions.get(2))
|
||||
.extracting(Transition::getType)
|
||||
.isEqualTo("withJunction");
|
||||
.isEqualTo(TransitionType.JUNCTION);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -244,12 +256,14 @@ class AstTransitionParserTest {
|
||||
Transition transition = transitions.getFirst();
|
||||
assertThat(transition)
|
||||
.extracting(Transition::getType)
|
||||
.isEqualTo("withInternal");
|
||||
.isEqualTo(TransitionType.INTERNAL);
|
||||
assertThat(transition.getSourceStates()).containsExactly("PAID");
|
||||
assertThat(transition)
|
||||
.extracting(Transition::getEvent)
|
||||
.isEqualTo("UPDATE");
|
||||
assertThat(transition.getActions()).containsExactly("updateStatus()");
|
||||
assertThat(transition.getActions())
|
||||
.extracting(Action::expression)
|
||||
.containsExactly("updateStatus()");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -269,7 +283,7 @@ class AstTransitionParserTest {
|
||||
Transition transition = transitions.getFirst();
|
||||
assertThat(transition)
|
||||
.extracting(Transition::getType)
|
||||
.isEqualTo("withLocal");
|
||||
.isEqualTo(TransitionType.LOCAL);
|
||||
assertThat(transition.getSourceStates()).containsExactly("PAID");
|
||||
assertThat(transition.getTargetStates()).containsExactly("PROCESSING");
|
||||
assertThat(transition)
|
||||
@@ -294,7 +308,7 @@ class AstTransitionParserTest {
|
||||
Transition transition = transitions.getFirst();
|
||||
assertThat(transition)
|
||||
.extracting(Transition::getType)
|
||||
.isEqualTo("withFork");
|
||||
.isEqualTo(TransitionType.FORK);
|
||||
assertThat(transition.getSourceStates()).containsExactly("PAID");
|
||||
assertThat(transition.getTargetStates()).containsExactlyInAnyOrder("SHIPPING", "BILLING");
|
||||
}
|
||||
@@ -316,7 +330,7 @@ class AstTransitionParserTest {
|
||||
Transition transition = transitions.getFirst();
|
||||
assertThat(transition)
|
||||
.extracting(Transition::getType)
|
||||
.isEqualTo("withJoin");
|
||||
.isEqualTo(TransitionType.JOIN);
|
||||
assertThat(transition.getSourceStates()).containsExactlyInAnyOrder("SHIPPING", "BILLING");
|
||||
assertThat(transition.getTargetStates()).containsExactly("COMPLETED");
|
||||
}
|
||||
@@ -416,9 +430,15 @@ class AstTransitionParserTest {
|
||||
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
|
||||
|
||||
assertThat(transitions).hasSize(3);
|
||||
assertThat(transitions.getFirst().getActions()).containsExactly("shipExpress()");
|
||||
assertThat(transitions.get(1).getActions()).containsExactly("cancelOrder()");
|
||||
assertThat(transitions.get(2).getActions()).containsExactly("defaultAction()");
|
||||
assertThat(transitions.getFirst().getActions())
|
||||
.extracting(Action::expression)
|
||||
.containsExactly("shipExpress()");
|
||||
assertThat(transitions.get(1).getActions())
|
||||
.extracting(Action::expression)
|
||||
.containsExactly("cancelOrder()");
|
||||
assertThat(transitions.get(2).getActions())
|
||||
.extracting(Action::expression)
|
||||
.containsExactly("defaultAction()");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -436,8 +456,10 @@ class AstTransitionParserTest {
|
||||
|
||||
assertThat(transitions).hasSize(1);
|
||||
Transition transition = transitions.getFirst();
|
||||
assertThat(transition.isLambdaGuard()).isTrue();
|
||||
assertThat(transition.getIsLambdaActions()).containsExactly(true);
|
||||
assertThat(transition.getGuard().isLambda()).isTrue();
|
||||
assertThat(transition.getActions())
|
||||
.extracting(Action::isLambda)
|
||||
.containsExactly(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -465,7 +487,9 @@ class AstTransitionParserTest {
|
||||
|
||||
assertThat(transitions).hasSize(1);
|
||||
Transition transition = transitions.getFirst();
|
||||
assertThat(transition.getIsLambdaActions()).containsExactly(true);
|
||||
assertThat(transition.getActions())
|
||||
.extracting(Action::isLambda)
|
||||
.containsExactly(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -272,6 +272,172 @@ class StateMachineAggregatorTest {
|
||||
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreStatesInsideFork() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||
public class ForkStateConfig extends StateMachineConfigurerAdapter {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial("CREATED")
|
||||
.fork("PROCESSING")
|
||||
.and()
|
||||
.withStates()
|
||||
.parent("PROCESSING")
|
||||
.initial("SHIPPING")
|
||||
.initial("BILLING")
|
||||
.end("COMPLETED");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
writeClass("ForkStateConfig", source);
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("ForkStateConfig");
|
||||
aggregator.aggregateStates(td);
|
||||
|
||||
assertThat(aggregator.getInitialStates()).containsExactly("CREATED");
|
||||
assertThat(aggregator.getEndStates()).containsExactly("COMPLETED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleNestedForkAndRegion() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||
public class ComplexStateConfig extends StateMachineConfigurerAdapter {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial("CREATED")
|
||||
.fork("PROCESSING")
|
||||
.and()
|
||||
.withStates()
|
||||
.parent("PROCESSING")
|
||||
.region("SHIPPING")
|
||||
.and()
|
||||
.withStates()
|
||||
.parent("SHIPPING")
|
||||
.initial("PREPARING")
|
||||
.end("SHIPPED")
|
||||
.and()
|
||||
.withStates()
|
||||
.parent("PROCESSING")
|
||||
.region("BILLING")
|
||||
.and()
|
||||
.withStates()
|
||||
.parent("BILLING")
|
||||
.initial("CALCULATING")
|
||||
.end("BILLED")
|
||||
.and()
|
||||
.withStates()
|
||||
.parent("PROCESSING")
|
||||
.end("COMPLETED");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
writeClass("ComplexStateConfig", source);
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("ComplexStateConfig");
|
||||
aggregator.aggregateStates(td);
|
||||
|
||||
assertThat(aggregator.getInitialStates()).containsExactly("CREATED");
|
||||
assertThat(aggregator.getEndStates()).containsExactlyInAnyOrder("COMPLETED", "SHIPPED", "BILLED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFollowMethodCallsInOtherClasses() throws IOException {
|
||||
String otherClassSource = """
|
||||
package com.example;
|
||||
public class ExternalTransitions {
|
||||
public static void addCommon(StateMachineTransitionConfigurer transitions) {
|
||||
transitions.withExternal().source("S1").target("S2").event("EXT_E1");
|
||||
}
|
||||
}
|
||||
""";
|
||||
String mainSource = """
|
||||
package com.example;
|
||||
public class MainConfig extends StateMachineConfigurerAdapter {
|
||||
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
|
||||
ExternalTransitions.addCommon(transitions);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
writeClass("ExternalTransitions", otherClassSource);
|
||||
writeClass("MainConfig", mainSource);
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("MainConfig");
|
||||
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
||||
|
||||
// Currently StateMachineAggregator only searches within the hierarchy of the searchStartClass.
|
||||
// It might not find ExternalTransitions.addCommon unless it's static or we handle imports.
|
||||
// Let's see if it works or if we need to improve the aggregator.
|
||||
assertThat(transitions).hasSize(1);
|
||||
assertThat(transitions.get(0).getEvent()).isEqualTo("EXT_E1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleRecursionSafety() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class RecursiveConfig extends StateMachineConfigurerAdapter {
|
||||
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
|
||||
transitions.withExternal().source("S1").target("S2").event("E1");
|
||||
recursiveMethod(transitions);
|
||||
}
|
||||
private void recursiveMethod(StateMachineTransitionConfigurer transitions) {
|
||||
transitions.withExternal().source("S2").target("S3").event("E2");
|
||||
recursiveMethod(transitions);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
writeClass("RecursiveConfig", source);
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("RecursiveConfig");
|
||||
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
||||
|
||||
// Should not crash and should find both transitions
|
||||
assertThat(transitions).hasSize(2);
|
||||
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E1"));
|
||||
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseTransitionsInsideTryBlock() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class TryConfig extends StateMachineConfigurerAdapter {
|
||||
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
|
||||
try {
|
||||
transitions.withExternal().source("S1").target("S2").event("TRY_E1");
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
writeClass("TryConfig", source);
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("TryConfig");
|
||||
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
||||
|
||||
assertThat(transitions).hasSize(1);
|
||||
assertThat(transitions.get(0).getEvent()).isEqualTo("TRY_E1");
|
||||
}
|
||||
|
||||
private void writeClass(String name, String source) throws IOException {
|
||||
Files.writeString(tempDir.resolve(name + ".java"), source);
|
||||
}
|
||||
|
||||
@@ -1,392 +0,0 @@
|
||||
package click.kamil.springstatemachineexporter.ast.app;
|
||||
|
||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class StateMachineStateConfigurationMethodFinderTest {
|
||||
|
||||
@Test
|
||||
void shouldParseInitialStates() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(OrderStates.CREATED)
|
||||
.end(OrderStates.COMPLETED);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
|
||||
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseMultipleInitialStates() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(OrderStates.CREATED)
|
||||
.initial(OrderStates.PENDING)
|
||||
.end(OrderStates.COMPLETED)
|
||||
.end(OrderStates.CANCELLED);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).containsExactlyInAnyOrder("OrderStates.CREATED", "OrderStates.PENDING");
|
||||
assertThat(visitor.getEndStates()).containsExactlyInAnyOrder("OrderStates.COMPLETED", "OrderStates.CANCELLED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreStatesInsideFork() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(OrderStates.CREATED)
|
||||
.fork(OrderStates.PROCESSING)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(OrderStates.PROCESSING)
|
||||
.initial(OrderStates.SHIPPING)
|
||||
.initial(OrderStates.BILLING)
|
||||
.end(OrderStates.COMPLETED);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
|
||||
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreStatesInsideRegion() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(OrderStates.CREATED)
|
||||
.region(OrderStates.PROCESSING)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(OrderStates.PROCESSING)
|
||||
.initial(OrderStates.SHIPPING)
|
||||
.initial(OrderStates.BILLING)
|
||||
.end(OrderStates.COMPLETED);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
|
||||
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseStatesWithStringLiterals() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<String, String> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial("CREATED")
|
||||
.end("COMPLETED");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).containsExactly("CREATED");
|
||||
assertThat(visitor.getEndStates()).containsExactly("COMPLETED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseStatesWithQuotedStrings() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<String, String> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial("CREATED")
|
||||
.end("COMPLETED");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).containsExactly("CREATED");
|
||||
assertThat(visitor.getEndStates()).containsExactly("COMPLETED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreNonConfigureMethods() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
public void someOtherMethod() {
|
||||
states
|
||||
.withStates()
|
||||
.initial(OrderStates.CREATED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(OrderStates.PENDING)
|
||||
.end(OrderStates.COMPLETED);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.PENDING");
|
||||
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleEmptyConfigureMethod() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
// Empty method
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).isEmpty();
|
||||
assertThat(visitor.getEndStates()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleConfigureMethodWithoutWithStates() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
System.out.println("No state configuration here");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).isEmpty();
|
||||
assertThat(visitor.getEndStates()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseComplexStateConfiguration() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(OrderStates.CREATED)
|
||||
.state(OrderStates.PAID)
|
||||
.state(OrderStates.SHIPPED)
|
||||
.end(OrderStates.COMPLETED)
|
||||
.end(OrderStates.CANCELLED)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(OrderStates.PAID)
|
||||
.initial(OrderStates.PROCESSING)
|
||||
.end(OrderStates.READY);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).containsExactlyInAnyOrder("OrderStates.CREATED", "OrderStates.PROCESSING");
|
||||
assertThat(visitor.getEndStates()).containsExactlyInAnyOrder("OrderStates.COMPLETED", "OrderStates.CANCELLED", "OrderStates.READY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseStatesWithMethodCalls() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(getInitialState())
|
||||
.end(getEndState());
|
||||
}
|
||||
|
||||
private OrderStates getInitialState() {
|
||||
return OrderStates.CREATED;
|
||||
}
|
||||
|
||||
private OrderStates getEndState() {
|
||||
return OrderStates.COMPLETED;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).containsExactly("getInitialState()");
|
||||
assertThat(visitor.getEndStates()).containsExactly("getEndState()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleMultipleConfigureMethods() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(OrderStates.CREATED)
|
||||
.end(OrderStates.COMPLETED);
|
||||
}
|
||||
|
||||
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
|
||||
// This should be ignored
|
||||
states
|
||||
.withStates()
|
||||
.initial(OrderStates.PENDING);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).containsExactlyInAnyOrder("OrderStates.CREATED", "OrderStates.PENDING");
|
||||
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseStatesWithEnumValues() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(OrderStates.CREATED)
|
||||
.state(OrderStates.PAID)
|
||||
.state(OrderStates.SHIPPED)
|
||||
.end(OrderStates.COMPLETED);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
|
||||
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleNestedForkAndRegion() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(OrderStates.CREATED)
|
||||
.fork(OrderStates.PROCESSING)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(OrderStates.PROCESSING)
|
||||
.region(OrderStates.SHIPPING)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(OrderStates.SHIPPING)
|
||||
.initial(OrderStates.PREPARING)
|
||||
.end(OrderStates.SHIPPED)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(OrderStates.PROCESSING)
|
||||
.region(OrderStates.BILLING)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(OrderStates.BILLING)
|
||||
.initial(OrderStates.CALCULATING)
|
||||
.end(OrderStates.BILLED)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent(OrderStates.PROCESSING)
|
||||
.end(OrderStates.COMPLETED);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
|
||||
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||
cu.accept(visitor);
|
||||
|
||||
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
|
||||
assertThat(visitor.getEndStates()).containsExactlyInAnyOrder("OrderStates.COMPLETED", "OrderStates.SHIPPED", "OrderStates.BILLED");
|
||||
}
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
package click.kamil.springstatemachineexporter.ast.app;
|
||||
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.SimpleName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class StateMachineTransitionConfigurationMethodFinderTest {
|
||||
|
||||
@Test
|
||||
void shouldFindConfigureMethodInEnumStateMachineConfigurerAdapter() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source(OrderStates.CREATED)
|
||||
.target(OrderStates.PAID)
|
||||
.event(OrderEvents.PAY);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method)
|
||||
.extracting(MethodDeclaration::getName)
|
||||
.extracting(SimpleName::getIdentifier)
|
||||
.isEqualTo("configure");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindConfigureMethodInStateMachineConfigurerAdapter() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source("CREATED")
|
||||
.target("PAID")
|
||||
.event("PAY");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method)
|
||||
.extracting(MethodDeclaration::getName)
|
||||
.extracting(SimpleName::getIdentifier)
|
||||
.isEqualTo("configure");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNull_whenClassDoesNotExtendStateMachineConfigurer() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig {
|
||||
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
|
||||
// This should not be found
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNull_whenClassIsInterface() {
|
||||
String source = """
|
||||
public interface OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception;
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNull_whenConfigureMethodIsNotPublic() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
protected void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
|
||||
// This should not be found
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNull_whenConfigureMethodDoesNotReturnVoid() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public String configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
|
||||
// This should not be found
|
||||
return "result";
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNull_whenConfigureMethodHasWrongNumberOfParameters() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure() throws Exception {
|
||||
// This should not be found
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNull_whenConfigureMethodParameterIsNotStateMachineTransitionConfigurer() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
// This should not be found
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNull_whenConfigureMethodDoesNotThrowException() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) {
|
||||
// This should not be found
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNull_whenConfigureMethodThrowsDifferentException() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws RuntimeException {
|
||||
// This should not be found
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindConfigureMethodWithMultipleExceptions() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception, RuntimeException {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source(OrderStates.CREATED)
|
||||
.target(OrderStates.PAID)
|
||||
.event(OrderEvents.PAY);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method)
|
||||
.extracting(MethodDeclaration::getName)
|
||||
.extracting(SimpleName::getIdentifier)
|
||||
.isEqualTo("configure");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindConfigureMethodWithParameterizedType() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source(OrderStates.CREATED)
|
||||
.target(OrderStates.PAID)
|
||||
.event(OrderEvents.PAY);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method)
|
||||
.extracting(MethodDeclaration::getName)
|
||||
.extracting(SimpleName::getIdentifier)
|
||||
.isEqualTo("configure");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNull_whenMultipleConfigureMethodsExist() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
// State configuration
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
|
||||
// Transition configuration
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method)
|
||||
.extracting(MethodDeclaration::getName)
|
||||
.extracting(SimpleName::getIdentifier)
|
||||
.isEqualTo("configure");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleClassWithNoMethods() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
// No methods
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleClassWithOtherMethods() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
public void someOtherMethod() {
|
||||
// This should be ignored
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
|
||||
// This should be found
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method)
|
||||
.extracting(MethodDeclaration::getName)
|
||||
.extracting(SimpleName::getIdentifier)
|
||||
.isEqualTo("configure");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleClassWithGenericParameters() {
|
||||
String source = """
|
||||
public class OrderStateMachineConfig<S, E> extends EnumStateMachineConfigurerAdapter<S, E> {
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<S, E> transitions) throws Exception {
|
||||
// Generic configuration
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method)
|
||||
.extracting(MethodDeclaration::getName)
|
||||
.extracting(SimpleName::getIdentifier)
|
||||
.isEqualTo("configure");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleClassWithAnnotations() {
|
||||
String source = """
|
||||
@Configuration
|
||||
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
@Override
|
||||
@Bean
|
||||
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
|
||||
// Configuration with annotations
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
|
||||
MethodDeclaration method = finder.findConfigureMethod();
|
||||
|
||||
assertThat(method)
|
||||
.extracting(MethodDeclaration::getName)
|
||||
.extracting(SimpleName::getIdentifier)
|
||||
.isEqualTo("configure");
|
||||
}
|
||||
}
|
||||
@@ -97,4 +97,53 @@ class CodebaseContextTest {
|
||||
assertThat(entryPoints).hasSize(1);
|
||||
assertThat(entryPoints.get(0).getName().getIdentifier()).isEqualTo("LeafConfig");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindBeanMethodsReturningStateMachine() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
public class MyApp {
|
||||
@Bean
|
||||
public StateMachine<String, String> stateMachine() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public StateMachine<String, String> notABean() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("MyApp.java"), source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
var beanMethods = context.findBeanMethodsReturning(java.util.Set.of("StateMachine"));
|
||||
assertThat(beanMethods).hasSize(1);
|
||||
assertThat(beanMethods.get(0).getName().getIdentifier()).isEqualTo("stateMachine");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindBeanMethodsReturningStateMachineWithQualifiedName() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
public class MyApp {
|
||||
@Bean
|
||||
public org.springframework.statemachine.StateMachine stateMachine() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("MyApp.java"), source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
var beanMethods = context.findBeanMethodsReturning(java.util.Set.of("StateMachine"));
|
||||
assertThat(beanMethods).hasSize(1);
|
||||
assertThat(beanMethods.get(0).getName().getIdentifier()).isEqualTo("stateMachine");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
package click.kamil.springstatemachineexporter.ast.in;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
|
||||
|
||||
class AstFileFinderTest {
|
||||
|
||||
private static final String CLASS_WITH_ANNOTATION = """
|
||||
@Service
|
||||
public class TestService {
|
||||
public void doSomething() {
|
||||
// implementation
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String CLASS_WITH_MULTIPLE_ANNOTATIONS = """
|
||||
@Service
|
||||
@Component
|
||||
public class TestService {
|
||||
public void doSomething() {
|
||||
// implementation
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String INTERFACE_WITH_ANNOTATION = """
|
||||
@FunctionalInterface
|
||||
public interface TestInterface {
|
||||
void doSomething();
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String CLASS_WITHOUT_ANNOTATION = """
|
||||
public class TestService {
|
||||
public void doSomething() {
|
||||
// implementation
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String CLASS_WITH_DIFFERENT_ANNOTATION = """
|
||||
@Repository
|
||||
public class TestRepository {
|
||||
public void doSomething() {
|
||||
// implementation
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String CLASS_WITH_FULLY_QUALIFIED_ANNOTATION = """
|
||||
@org.springframework.stereotype.Service
|
||||
public class TestService {
|
||||
public void doSomething() {
|
||||
// implementation
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String MULTIPLE_CLASSES = """
|
||||
@Service
|
||||
public class TestService {
|
||||
public void doSomething() {
|
||||
// implementation
|
||||
}
|
||||
}
|
||||
|
||||
public class AnotherClass {
|
||||
// no annotation
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String CLASS_WITH_ANNOTATION_AND_FIELDS = """
|
||||
@Service
|
||||
public class TestService {
|
||||
private String name;
|
||||
|
||||
public void doSomething() {
|
||||
// implementation
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String EMPTY_SOURCE = "";
|
||||
|
||||
private static final String INVALID_JAVA_SYNTAX = """
|
||||
@Service
|
||||
public class TestService {
|
||||
// missing closing brace
|
||||
""";
|
||||
|
||||
@Test
|
||||
void shouldReturnTrue_whenClassHasMatchingAnnotation() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("Service");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
|
||||
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrue_whenClassHasMultipleMatchingAnnotations() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("Service", "Component");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_MULTIPLE_ANNOTATIONS, annotationNames);
|
||||
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFalse_whenClassHasNoAnnotation() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("Service");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(CLASS_WITHOUT_ANNOTATION, annotationNames);
|
||||
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFalse_whenClassHasDifferentAnnotation() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("Service");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_DIFFERENT_ANNOTATION, annotationNames);
|
||||
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrue_whenClassHasFullyQualifiedAnnotation() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("org.springframework.stereotype.Service");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_FULLY_QUALIFIED_ANNOTATION, annotationNames);
|
||||
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrue_whenMultipleClassesAndOneHasAnnotation() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("Service");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(MULTIPLE_CLASSES, annotationNames);
|
||||
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFalse_whenInterfaceHasAnnotation() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("FunctionalInterface");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(INTERFACE_WITH_ANNOTATION, annotationNames);
|
||||
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFalse_whenEmptyAnnotationList() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = Collections.emptyList();
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
|
||||
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFalse_whenEmptySource() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("Service");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(EMPTY_SOURCE, annotationNames);
|
||||
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowException_whenNullAnnotationList() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
|
||||
// when & then
|
||||
assertThatThrownBy(() ->
|
||||
finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, null)
|
||||
).isInstanceOf(NullPointerException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrue_whenClassHasAnnotationAndFields() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("Service");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION_AND_FIELDS, annotationNames);
|
||||
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleInvalidJavaSyntax() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("Service");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(INVALID_JAVA_SYNTAX, annotationNames);
|
||||
|
||||
// AST parser might still create a valid AST even with syntax errors
|
||||
// The result depends on how the parser handles the invalid syntax
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrue_whenAnnotationNameMatchesCaseInsensitive() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("service"); // lowercase
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
|
||||
|
||||
assertThat(result).isFalse(); // AST parser is case-sensitive
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrue_whenMultipleAnnotationNamesProvided() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("Component", "Service", "Repository");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
|
||||
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFalse_whenNoMatchingAnnotationNames() {
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("Component", "Repository", "Controller");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
|
||||
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFalse_whenSourceContainsOnlyComments() {
|
||||
String sourceWithComments = """
|
||||
// This is a comment
|
||||
/* Another comment */
|
||||
/** JavaDoc comment */
|
||||
""";
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("Service");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(sourceWithComments, annotationNames);
|
||||
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFalse_whenSourceContainsOnlyPackageDeclaration() {
|
||||
String sourceWithPackage = "package com.example;";
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
List<String> annotationNames = List.of("Service");
|
||||
|
||||
boolean result = finder.hasClassWithAnnotation(sourceWithPackage, annotationNames);
|
||||
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrue_whenFunctionReturnsTargetType() {
|
||||
String source = """
|
||||
public class Test {
|
||||
@Bean
|
||||
public StateMachine<S, E> build() { return null; }
|
||||
}
|
||||
""";
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
boolean result = finder.hasFunctionReturningType(source, Set.of("StateMachine"));
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFalse_whenNoFunctionReturnsTargetType() {
|
||||
String source = """
|
||||
public class Test {
|
||||
@Bean
|
||||
public void build() {}
|
||||
}
|
||||
""";
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
boolean result = finder.hasFunctionReturningType(source, Set.of("StateMachine"));
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTrue_whenFunctionReturnsQualifiedTargetType() {
|
||||
String source = """
|
||||
public class Test {
|
||||
@Bean
|
||||
public org.springframework.statemachine.StateMachine build() { return null; }
|
||||
}
|
||||
""";
|
||||
AstFileFinder finder = new AstFileFinder();
|
||||
boolean result = finder.hasFunctionReturningType(source, Set.of("StateMachine"));
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user