updated
This commit is contained in:
@@ -92,7 +92,7 @@ public class Main {
|
|||||||
if (!methodsWithReturnType.isEmpty()) {
|
if (!methodsWithReturnType.isEmpty()) {
|
||||||
for (MethodDeclaration m : methodsWithReturnType) {
|
for (MethodDeclaration m : methodsWithReturnType) {
|
||||||
System.out.println("Found method returning StateMachine in: " + javaFile.toAbsolutePath());
|
System.out.println("Found method returning StateMachine in: " + javaFile.toAbsolutePath());
|
||||||
List<Transition> transitions = AstTransitionParser.parseTransitions2(m);
|
List<Transition> transitions = AstTransitionParser.parseTransitions(m);
|
||||||
|
|
||||||
Set<String> startStatesTransitions = TransitionStateUtils.findStartStates(transitions);
|
Set<String> startStatesTransitions = TransitionStateUtils.findStartStates(transitions);
|
||||||
Set<String> endStatesTransitions = TransitionStateUtils.findEndStates(transitions);
|
Set<String> endStatesTransitions = TransitionStateUtils.findEndStates(transitions);
|
||||||
|
|||||||
@@ -3,55 +3,36 @@ package click.kamil.springstatemachineexporter.ast.app;
|
|||||||
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
|
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class AstTransitionParser {
|
public class AstTransitionParser {
|
||||||
|
|
||||||
private static final Set<String> SUPPORTED_TRANSITION_TYPES = Set.of(
|
private static final Set<String> SUPPORTED_TRANSITION_TYPES = Set.of(
|
||||||
"withExternal", "withInternal", "withLocal", "withFork", "withJoin", "withChoice", "withJunction"
|
"withExternal", "withInternal", "withLocal", "withFork", "withJoin", "withChoice", "withJunction"
|
||||||
);
|
);
|
||||||
|
|
||||||
public static List<Transition> parseTransitions(MethodDeclaration method) {
|
public static List<Transition> parseTransitions(MethodDeclaration method) {
|
||||||
List<Transition> transitions = new ArrayList<>();
|
if (method == null) return Collections.emptyList();
|
||||||
Optional.ofNullable(method)
|
|
||||||
.map(MethodDeclaration::getBody)
|
|
||||||
.ifPresent(body -> {
|
|
||||||
for (Object stmt : body.statements()) {
|
|
||||||
if (stmt instanceof ExpressionStatement es
|
|
||||||
&& es.getExpression() instanceof MethodInvocation mi) {
|
|
||||||
transitions.addAll(parseTransitionsFromExpression(mi));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return transitions;
|
|
||||||
}
|
|
||||||
public static List<Transition> parseTransitions2(MethodDeclaration method) {
|
|
||||||
List<Transition> transitions = new ArrayList<>();
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
|
||||||
Optional.ofNullable(method)
|
method.accept(new ASTVisitor() {
|
||||||
.map(MethodDeclaration::getBody)
|
@Override
|
||||||
.ifPresent(body -> {
|
public boolean visit(MethodInvocation node) {
|
||||||
for (Object stmtObj : body.statements()) {
|
if (isEntryPoint(node)) {
|
||||||
if (stmtObj instanceof ExpressionStatement es
|
transitions.addAll(parseTransitionsFromExpression(node, new HashMap<>()));
|
||||||
&& es.getExpression() instanceof MethodInvocation mi) {
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
// recursively collect all method invocations in this chain
|
private boolean isEntryPoint(MethodInvocation node) {
|
||||||
List<MethodInvocation> chain = collectMethodInvocationChain(mi);
|
// Check if this call is part of a larger chain already handled
|
||||||
|
if (node.getParent() instanceof MethodInvocation) {
|
||||||
// Find the index of `configureTransitions` in the chain
|
return false;
|
||||||
for (int i = 0; i < chain.size(); i++) {
|
}
|
||||||
MethodInvocation current = chain.get(i);
|
List<MethodInvocation> chain = collectMethodInvocationChain(node);
|
||||||
if (current.getName().getIdentifier().equals("configureTransitions")) {
|
return chain.stream().anyMatch(mi -> SUPPORTED_TRANSITION_TYPES.contains(mi.getName().getIdentifier()));
|
||||||
List<MethodInvocation> afterConfigure = chain.subList(i + 1, chain.size());
|
}
|
||||||
transitions.addAll(parseTransitionsFromExpression(afterConfigure.getLast()));
|
});
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return transitions;
|
return transitions;
|
||||||
}
|
}
|
||||||
@@ -74,7 +55,7 @@ public class AstTransitionParser {
|
|||||||
return chain;
|
return chain;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Transition> parseTransitionsFromExpression(MethodInvocation rootCall) {
|
public static List<Transition> parseTransitionsFromExpression(MethodInvocation rootCall, Map<String, Expression> argsMap) {
|
||||||
List<Transition> transitions = new ArrayList<>();
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
|
||||||
// Step 1: Unroll method chain
|
// Step 1: Unroll method chain
|
||||||
@@ -106,16 +87,16 @@ public class AstTransitionParser {
|
|||||||
boolean isJunction = segment.stream().anyMatch(mi -> "withJunction".equals(mi.getName().getIdentifier()));
|
boolean isJunction = segment.stream().anyMatch(mi -> "withJunction".equals(mi.getName().getIdentifier()));
|
||||||
|
|
||||||
if (isChoice || isJunction) {
|
if (isChoice || isJunction) {
|
||||||
transitions.addAll(parseChoiceOrJunctionTransitions(segment, isChoice ? "withChoice" : "withJunction"));
|
transitions.addAll(parseChoiceOrJunctionTransitions(segment, isChoice ? "withChoice" : "withJunction", argsMap));
|
||||||
} else {
|
} else {
|
||||||
transitions.add(parseStandardTransition(segment));
|
transitions.add(parseStandardTransition(segment, argsMap));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return transitions;
|
return transitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Transition> parseChoiceOrJunctionTransitions(List<MethodInvocation> segment, String type) {
|
private static List<Transition> parseChoiceOrJunctionTransitions(List<MethodInvocation> segment, String type, Map<String, Expression> argsMap) {
|
||||||
List<Transition> transitions = new ArrayList<>();
|
List<Transition> transitions = new ArrayList<>();
|
||||||
String sourceState = null;
|
String sourceState = null;
|
||||||
int orderCounter = 0;
|
int orderCounter = 0;
|
||||||
@@ -123,7 +104,8 @@ public class AstTransitionParser {
|
|||||||
// Extract source state once
|
// Extract source state once
|
||||||
for (MethodInvocation call : segment) {
|
for (MethodInvocation call : segment) {
|
||||||
if ("source".equals(call.getName().getIdentifier()) && !call.arguments().isEmpty()) {
|
if ("source".equals(call.getName().getIdentifier()) && !call.arguments().isEmpty()) {
|
||||||
sourceState = QuotedExpression.of(call.arguments().getFirst()).toStringWithoutQuotes();
|
Expression arg = resolveArg((Expression) call.arguments().get(0), argsMap);
|
||||||
|
sourceState = QuotedExpression.of(arg).toStringWithoutQuotes();
|
||||||
break; // only take the first occurrence
|
break; // only take the first occurrence
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,18 +121,23 @@ public class AstTransitionParser {
|
|||||||
if (sourceState != null) {
|
if (sourceState != null) {
|
||||||
t.getSourceStates().add(sourceState);
|
t.getSourceStates().add(sourceState);
|
||||||
}
|
}
|
||||||
t.getTargetStates().add(QuotedExpression.of(args.get(0)).toStringWithoutQuotes());
|
|
||||||
|
Expression targetArg = resolveArg((Expression) args.get(0), argsMap);
|
||||||
|
t.getTargetStates().add(QuotedExpression.of(targetArg).toStringWithoutQuotes());
|
||||||
|
|
||||||
if (args.size() > 1) {
|
if (args.size() > 1) {
|
||||||
|
Expression secondArg = resolveArg((Expression) args.get(1), argsMap);
|
||||||
if ("last".equals(methodName)) {
|
if ("last".equals(methodName)) {
|
||||||
// For 'last', the second argument is an action, not a guard
|
// For 'last', the second argument is an action, not a guard
|
||||||
parseAction(args.get(1), t);
|
parseAction(secondArg, t);
|
||||||
} else {
|
} else {
|
||||||
// For 'first' and 'then', the second argument is a guard
|
// For 'first' and 'then', the second argument is a guard
|
||||||
parseGuard(args.get(1), t);
|
parseGuard(secondArg, t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (args.size() > 2) {
|
if (args.size() > 2) {
|
||||||
parseAction(args.get(2), t);
|
Expression thirdArg = resolveArg((Expression) args.get(2), argsMap);
|
||||||
|
parseAction(thirdArg, t);
|
||||||
}
|
}
|
||||||
t.setOrder(orderCounter++);
|
t.setOrder(orderCounter++);
|
||||||
transitions.add(t);
|
transitions.add(t);
|
||||||
@@ -159,7 +146,7 @@ public class AstTransitionParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static Transition parseStandardTransition(List<MethodInvocation> segment) {
|
private static Transition parseStandardTransition(List<MethodInvocation> segment, Map<String, Expression> argsMap) {
|
||||||
Transition t = new Transition();
|
Transition t = new Transition();
|
||||||
for (MethodInvocation call : segment) {
|
for (MethodInvocation call : segment) {
|
||||||
String methodName = call.getName().getIdentifier();
|
String methodName = call.getName().getIdentifier();
|
||||||
@@ -174,11 +161,26 @@ public class AstTransitionParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (methodName) {
|
switch (methodName) {
|
||||||
case "source" -> args.forEach(arg -> t.getSourceStates().add(QuotedExpression.of(arg).toStringWithoutQuotes()));
|
case "source" -> args.forEach(arg -> {
|
||||||
case "target" -> args.forEach(arg -> t.getTargetStates().add(QuotedExpression.of(arg).toStringWithoutQuotes()));
|
Expression resolved = resolveArg((Expression) arg, argsMap);
|
||||||
case "event" -> t.setEvent(QuotedExpression.of(args.getFirst()).toStringWithoutQuotes());
|
t.getSourceStates().add(QuotedExpression.of(resolved).toStringWithoutQuotes());
|
||||||
case "guard" -> parseGuard(args.getFirst(), t);
|
});
|
||||||
case "action" -> parseAction(args.getFirst(), t);
|
case "target" -> args.forEach(arg -> {
|
||||||
|
Expression resolved = resolveArg((Expression) arg, argsMap);
|
||||||
|
t.getTargetStates().add(QuotedExpression.of(resolved).toStringWithoutQuotes());
|
||||||
|
});
|
||||||
|
case "event" -> {
|
||||||
|
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
||||||
|
t.setEvent(QuotedExpression.of(resolved).toStringWithoutQuotes());
|
||||||
|
}
|
||||||
|
case "guard" -> {
|
||||||
|
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
||||||
|
parseGuard(resolved, t);
|
||||||
|
}
|
||||||
|
case "action" -> {
|
||||||
|
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
||||||
|
parseAction(resolved, t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
@@ -200,6 +202,14 @@ public class AstTransitionParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Expression resolveArg(Expression expr, Map<String, Expression> argsMap) {
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
Expression resolved = argsMap.get(sn.getIdentifier());
|
||||||
|
if (resolved != null) return resolved;
|
||||||
|
}
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isLambdaOrAnonymous(Expression expr) {
|
public static boolean isLambdaOrAnonymous(Expression expr) {
|
||||||
return switch (expr) {
|
return switch (expr) {
|
||||||
case null -> false;
|
case null -> false;
|
||||||
@@ -208,4 +218,4 @@ public class AstTransitionParser {
|
|||||||
default -> false;
|
default -> false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,33 +15,30 @@ public class StateMachineAggregator {
|
|||||||
|
|
||||||
public List<Transition> aggregateTransitions(TypeDeclaration startClass) {
|
public List<Transition> aggregateTransitions(TypeDeclaration startClass) {
|
||||||
List<Transition> allTransitions = new ArrayList<>();
|
List<Transition> allTransitions = new ArrayList<>();
|
||||||
Set<String> visitedClasses = new HashSet<>();
|
aggregateTransitionsRecursive(startClass, startClass, allTransitions, new HashSet<>(), new HashSet<>(), new HashMap<>());
|
||||||
aggregateTransitionsRecursive(startClass, startClass, allTransitions, visitedClasses, new HashSet<>());
|
|
||||||
return allTransitions;
|
return allTransitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodDeclaration> visitedMethods) {
|
private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodDeclaration> visitedMethods, Map<String, Object> argsMap) {
|
||||||
if (currentTd == null || visitedClasses.contains(currentTd.getName().getFullyQualifiedName())) return;
|
if (currentTd == null || visitedClasses.contains(currentTd.getName().getFullyQualifiedName())) return;
|
||||||
visitedClasses.add(currentTd.getName().getFullyQualifiedName());
|
visitedClasses.add(currentTd.getName().getFullyQualifiedName());
|
||||||
|
|
||||||
// Process this class
|
|
||||||
MethodDeclaration configureMethod = findConfigureTransitionsMethod(currentTd);
|
MethodDeclaration configureMethod = findConfigureTransitionsMethod(currentTd);
|
||||||
if (configureMethod != null) {
|
if (configureMethod != null) {
|
||||||
allTransitions.addAll(parseTransitionsWithMethodCalls(configureMethod, searchStartClass, visitedMethods));
|
allTransitions.addAll(parseTransitionsWithMethodCalls(configureMethod, searchStartClass, visitedMethods, argsMap));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process superclass
|
|
||||||
Type superclassType = currentTd.getSuperclassType();
|
Type superclassType = currentTd.getSuperclassType();
|
||||||
if (superclassType != null) {
|
if (superclassType != null) {
|
||||||
String superclassName = getSimpleName(superclassType);
|
String superclassName = getSimpleName(superclassType);
|
||||||
TypeDeclaration superclassTd = context.getTypeDeclaration(superclassName);
|
TypeDeclaration superclassTd = context.getTypeDeclaration(superclassName);
|
||||||
if (superclassTd != null) {
|
if (superclassTd != null) {
|
||||||
aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods);
|
aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Transition> parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<MethodDeclaration> visitedMethods) {
|
private List<Transition> parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<MethodDeclaration> visitedMethods, Map<String, Object> argsMap) {
|
||||||
List<Transition> transitions = new ArrayList<>();
|
List<Transition> transitions = new ArrayList<>();
|
||||||
if (visitedMethods.contains(method)) return transitions;
|
if (visitedMethods.contains(method)) return transitions;
|
||||||
visitedMethods.add(method);
|
visitedMethods.add(method);
|
||||||
@@ -50,51 +47,190 @@ public class StateMachineAggregator {
|
|||||||
if (body == null) return transitions;
|
if (body == null) return transitions;
|
||||||
|
|
||||||
for (Object stmt : body.statements()) {
|
for (Object stmt : body.statements()) {
|
||||||
Expression expr = null;
|
|
||||||
if (stmt instanceof ExpressionStatement es) {
|
if (stmt instanceof ExpressionStatement es) {
|
||||||
expr = es.getExpression();
|
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, argsMap));
|
||||||
} else if (stmt instanceof VariableDeclarationStatement vds) {
|
} else if (stmt instanceof VariableDeclarationStatement vds) {
|
||||||
for (Object fragmentObj : vds.fragments()) {
|
for (Object fragmentObj : vds.fragments()) {
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
||||||
expr = fragment.getInitializer();
|
transitions.addAll(parseTransitionsInExpression(fragment.getInitializer(), searchStartClass, visitedMethods, argsMap));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (stmt instanceof EnhancedForStatement efs) {
|
||||||
|
transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, true, null, null));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
if (expr instanceof MethodInvocation mi) {
|
private List<Transition> parseTransitionsInExpression(Expression expr, TypeDeclaration searchStartClass, Set<MethodDeclaration> visitedMethods, Map<String, Object> argsMap) {
|
||||||
if (isTransitionChain(mi)) {
|
List<Transition> transitions = new ArrayList<>();
|
||||||
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi));
|
if (expr instanceof MethodInvocation mi) {
|
||||||
} else {
|
if (isTransitionChain(mi)) {
|
||||||
// Possible method call to follow
|
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap)));
|
||||||
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
} else if (isForEach(mi)) {
|
||||||
if (calledMethod != null && isRelevantMethod(calledMethod)) {
|
transitions.addAll(parseForEach(mi, searchStartClass, visitedMethods, argsMap, true, null, null));
|
||||||
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, searchStartClass, visitedMethods));
|
} else {
|
||||||
}
|
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
||||||
|
if (calledMethod != null) {
|
||||||
|
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
||||||
|
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, searchStartClass, visitedMethods, nextArgsMap));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return transitions;
|
return transitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isForEach(MethodInvocation mi) {
|
||||||
|
return mi.getName().getIdentifier().equals("forEach");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Transition> parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set<MethodDeclaration> visitedMethods, Map<String, Object> argsMap, boolean isTransitions, Set<String> initialStates, Set<String> endStates) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
Expression receiver = forEachMi.getExpression();
|
||||||
|
List<Expression> elements = unrollCollection(receiver, argsMap);
|
||||||
|
if (elements.isEmpty() || forEachMi.arguments().isEmpty()) return transitions;
|
||||||
|
|
||||||
|
Object lambdaObj = forEachMi.arguments().get(0);
|
||||||
|
if (lambdaObj instanceof LambdaExpression lambda) {
|
||||||
|
String paramName = getLambdaParamName(lambda);
|
||||||
|
for (Expression element : elements) {
|
||||||
|
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
|
||||||
|
if (!paramName.isEmpty()) lambdaArgsMap.put(paramName, element);
|
||||||
|
|
||||||
|
if (isTransitions) {
|
||||||
|
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 {
|
||||||
|
if (lambda.getBody() instanceof Block block) parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||||
|
else if (lambda.getBody() instanceof Expression lambdaExpr) parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Transition> parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set<MethodDeclaration> visitedMethods, Map<String, Object> argsMap, boolean isTransitions, Set<String> initialStates, Set<String> endStates) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
List<Expression> elements = unrollCollection(efs.getExpression(), argsMap);
|
||||||
|
String paramName = efs.getParameter().getName().getIdentifier();
|
||||||
|
|
||||||
|
for (Expression element : elements) {
|
||||||
|
Map<String, Object> loopArgsMap = new HashMap<>(argsMap);
|
||||||
|
loopArgsMap.put(paramName, element);
|
||||||
|
|
||||||
|
if (isTransitions) {
|
||||||
|
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 {
|
||||||
|
if (efs.getBody() instanceof Block block) parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
|
||||||
|
else if (efs.getBody() instanceof ExpressionStatement es) parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Expression> unrollCollection(Expression expr, Map<String, Object> argsMap) {
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
String name = mi.getName().getIdentifier();
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
if ((name.equals("of") && receiver != null && (receiver.toString().equals("Stream") || receiver.toString().equals("List")))
|
||||||
|
|| (name.equals("asList") && receiver != null && receiver.toString().equals("Arrays"))) {
|
||||||
|
return (List<Expression>) mi.arguments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
Object resolved = argsMap.get(sn.getIdentifier());
|
||||||
|
if (resolved instanceof List list) return (List<Expression>) list;
|
||||||
|
if (resolved instanceof Expression e) return unrollCollection(e, argsMap);
|
||||||
|
}
|
||||||
|
if (expr instanceof ArrayInitializer ai) return (List<Expression>) ai.expressions();
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLambdaParamName(LambdaExpression lambda) {
|
||||||
|
if (lambda.parameters().size() == 1) {
|
||||||
|
Object p = lambda.parameters().get(0);
|
||||||
|
if (p instanceof VariableDeclaration vp) return vp.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Transition> parseTransitionsInBlock(Block block, TypeDeclaration searchStartClass, Set<MethodDeclaration> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
for (Object stmt : block.statements()) {
|
||||||
|
if (stmt instanceof ExpressionStatement es) transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, argsMap));
|
||||||
|
else if (stmt instanceof EnhancedForStatement efs) transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, true, null, null));
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseStatesInBlock(Block block, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodDeclaration> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
for (Object stmt : block.statements()) {
|
||||||
|
if (stmt instanceof ExpressionStatement es) parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
|
else if (stmt instanceof EnhancedForStatement efs) parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, false, initialStates, endStates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseStatesInExpression(Expression expr, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodDeclaration> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
if (isWithStatesChain(mi)) parseStateChain(mi, initialStates, endStates, argsMap);
|
||||||
|
else if (isForEach(mi)) parseForEach(mi, searchStartClass, visitedMethods, argsMap, false, initialStates, endStates);
|
||||||
|
else {
|
||||||
|
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
||||||
|
if (calledMethod != null) {
|
||||||
|
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
||||||
|
parseStatesWithMethodCalls(calledMethod, searchStartClass, initialStates, endStates, visitedMethods, nextArgsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildArgsMap(MethodDeclaration method, MethodInvocation call, Map<String, Object> currentArgsMap) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
List<?> params = method.parameters();
|
||||||
|
List<?> args = call.arguments();
|
||||||
|
|
||||||
|
for (int i = 0; i < params.size(); i++) {
|
||||||
|
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(i);
|
||||||
|
String paramName = param.getName().getIdentifier();
|
||||||
|
if (param.isVarargs() && i < params.size()) {
|
||||||
|
List<Expression> varargList = new ArrayList<>();
|
||||||
|
for (int j = i; j < args.size(); j++) {
|
||||||
|
varargList.add(resolveExpression((Expression) args.get(j), currentArgsMap));
|
||||||
|
}
|
||||||
|
map.put(paramName, varargList);
|
||||||
|
} else if (i < args.size()) {
|
||||||
|
map.put(paramName, resolveExpression((Expression) args.get(i), currentArgsMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression resolveExpression(Expression expr, Map<String, Object> argsMap) {
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
Object resolved = argsMap.get(sn.getIdentifier());
|
||||||
|
if (resolved instanceof Expression e) return e;
|
||||||
|
}
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Expression> convertToExpressionMap(Map<String, Object> argsMap) {
|
||||||
|
Map<String, Expression> map = new HashMap<>();
|
||||||
|
for (Map.Entry<String, Object> entry : argsMap.entrySet()) {
|
||||||
|
if (entry.getValue() instanceof Expression e) map.put(entry.getKey(), e);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isTransitionChain(MethodInvocation mi) {
|
private boolean isTransitionChain(MethodInvocation mi) {
|
||||||
String name = mi.getName().getIdentifier();
|
String name = mi.getName().getIdentifier();
|
||||||
if (Set.of("withExternal", "withInternal", "withLocal", "withFork", "withJoin", "withChoice", "withJunction").contains(name)) {
|
if (Set.of("withExternal", "withInternal", "withLocal", "withFork", "withJoin", "withChoice", "withJunction").contains(name)) return true;
|
||||||
return true;
|
return mi.getExpression() instanceof MethodInvocation next && isTransitionChain(next);
|
||||||
}
|
|
||||||
Expression expr = mi.getExpression();
|
|
||||||
if (expr instanceof MethodInvocation next) {
|
|
||||||
return isTransitionChain(next);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) {
|
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) {
|
||||||
if (td == null) return null;
|
if (td == null) return null;
|
||||||
for (MethodDeclaration method : td.getMethods()) {
|
for (MethodDeclaration method : td.getMethods()) if (method.getName().getIdentifier().equals(methodName)) return method;
|
||||||
if (method.getName().getIdentifier().equals(methodName)) {
|
|
||||||
return method;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Type superclassType = td.getSuperclassType();
|
Type superclassType = td.getSuperclassType();
|
||||||
if (superclassType != null) {
|
if (superclassType != null) {
|
||||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType));
|
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType));
|
||||||
@@ -103,89 +239,58 @@ public class StateMachineAggregator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isRelevantMethod(MethodDeclaration method) {
|
private boolean isRelevantMethod(MethodDeclaration method) { return true; }
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MethodDeclaration findConfigureTransitionsMethod(TypeDeclaration td) {
|
private MethodDeclaration findConfigureTransitionsMethod(TypeDeclaration td) {
|
||||||
for (MethodDeclaration method : td.getMethods()) {
|
for (MethodDeclaration method : td.getMethods()) if (isConfigureTransitionsMethod(method)) return method;
|
||||||
if (isConfigureTransitionsMethod(method)) return method;
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isConfigureTransitionsMethod(MethodDeclaration method) {
|
private boolean isConfigureTransitionsMethod(MethodDeclaration method) {
|
||||||
if (!method.getName().getIdentifier().equals("configure")) return false;
|
if (!method.getName().getIdentifier().equals("configure") || method.parameters().size() != 1) return false;
|
||||||
List<?> params = method.parameters();
|
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().startsWith("StateMachineTransitionConfigurer");
|
||||||
if (params.size() != 1) return false;
|
|
||||||
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(0);
|
|
||||||
String typeName = param.getType().toString();
|
|
||||||
return typeName.startsWith("StateMachineTransitionConfigurer");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void aggregateStates(TypeDeclaration startClass, Set<String> initialStates, Set<String> endStates) {
|
public void aggregateStates(TypeDeclaration startClass, Set<String> initialStates, Set<String> endStates) {
|
||||||
Set<String> visitedClasses = new HashSet<>();
|
aggregateStatesRecursive(startClass, startClass, initialStates, endStates, new HashSet<>(), new HashSet<>(), new HashMap<>());
|
||||||
aggregateStatesRecursive(startClass, startClass, initialStates, endStates, visitedClasses, new HashSet<>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodDeclaration> visitedMethods) {
|
private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodDeclaration> visitedMethods, Map<String, Object> argsMap) {
|
||||||
if (currentTd == null || visitedClasses.contains(currentTd.getName().getFullyQualifiedName())) return;
|
if (currentTd == null || visitedClasses.contains(currentTd.getName().getFullyQualifiedName())) return;
|
||||||
visitedClasses.add(currentTd.getName().getFullyQualifiedName());
|
visitedClasses.add(currentTd.getName().getFullyQualifiedName());
|
||||||
|
|
||||||
MethodDeclaration configureMethod = findConfigureStatesMethod(currentTd);
|
MethodDeclaration configureMethod = findConfigureStatesMethod(currentTd);
|
||||||
if (configureMethod != null) {
|
if (configureMethod != null) parseStatesWithMethodCalls(configureMethod, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
parseStatesWithMethodCalls(configureMethod, searchStartClass, initialStates, endStates, visitedMethods);
|
|
||||||
}
|
|
||||||
|
|
||||||
Type superclassType = currentTd.getSuperclassType();
|
Type superclassType = currentTd.getSuperclassType();
|
||||||
if (superclassType != null) {
|
if (superclassType != null) {
|
||||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType));
|
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType));
|
||||||
if (superclassTd != null) {
|
if (superclassTd != null) aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
|
||||||
aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodDeclaration> visitedMethods) {
|
private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodDeclaration> visitedMethods, Map<String, Object> argsMap) {
|
||||||
if (visitedMethods.contains(method)) return;
|
if (visitedMethods.contains(method)) return;
|
||||||
visitedMethods.add(method);
|
visitedMethods.add(method);
|
||||||
|
|
||||||
Block body = method.getBody();
|
Block body = method.getBody();
|
||||||
if (body == null) return;
|
if (body == null) return;
|
||||||
|
|
||||||
for (Object stmt : body.statements()) {
|
for (Object stmt : body.statements()) {
|
||||||
Expression expr = null;
|
if (stmt instanceof ExpressionStatement es) parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
if (stmt instanceof ExpressionStatement es) {
|
else if (stmt instanceof VariableDeclarationStatement vds) {
|
||||||
expr = es.getExpression();
|
|
||||||
} else if (stmt instanceof VariableDeclarationStatement vds) {
|
|
||||||
for (Object fragmentObj : vds.fragments()) {
|
for (Object fragmentObj : vds.fragments()) {
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
if (fragmentObj instanceof VariableDeclarationFragment fragment) parseStatesInExpression(fragment.getInitializer(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
expr = fragment.getInitializer();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
} else if (stmt instanceof EnhancedForStatement efs) parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, false, initialStates, endStates);
|
||||||
|
|
||||||
if (expr instanceof MethodInvocation mi) {
|
|
||||||
if (isWithStatesChain(mi)) {
|
|
||||||
parseStateChain(mi, initialStates, endStates);
|
|
||||||
} else {
|
|
||||||
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
|
||||||
if (calledMethod != null) {
|
|
||||||
parseStatesWithMethodCalls(calledMethod, searchStartClass, initialStates, endStates, visitedMethods);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isWithStatesChain(MethodInvocation mi) {
|
private boolean isWithStatesChain(MethodInvocation mi) {
|
||||||
if (mi.getName().getIdentifier().equals("withStates")) return true;
|
if (mi.getName().getIdentifier().equals("withStates")) return true;
|
||||||
Expression expr = mi.getExpression();
|
return mi.getExpression() instanceof MethodInvocation next && isWithStatesChain(next);
|
||||||
if (expr instanceof MethodInvocation next) return isWithStatesChain(next);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void parseStateChain(MethodInvocation mi, Set<String> initialStates, Set<String> endStates) {
|
private void parseStateChain(MethodInvocation mi, Set<String> initialStates, Set<String> endStates, Map<String, Object> argsMap) {
|
||||||
List<MethodInvocation> chain = new ArrayList<>();
|
List<MethodInvocation> chain = new ArrayList<>();
|
||||||
Expression current = mi;
|
Expression current = mi;
|
||||||
while (current instanceof MethodInvocation m) {
|
while (current instanceof MethodInvocation m) {
|
||||||
@@ -194,45 +299,30 @@ public class StateMachineAggregator {
|
|||||||
}
|
}
|
||||||
for (MethodInvocation call : chain) {
|
for (MethodInvocation call : chain) {
|
||||||
String methodName = call.getName().getIdentifier();
|
String methodName = call.getName().getIdentifier();
|
||||||
List<?> args = call.arguments();
|
if (call.arguments().isEmpty()) continue;
|
||||||
if (args.isEmpty()) continue;
|
Expression arg = resolveExpression((Expression) call.arguments().get(0), argsMap);
|
||||||
Expression arg = (Expression) args.get(0);
|
if ("initial".equals(methodName)) { if (!isInsideRegionOrFork(call)) initialStates.add(QuotedExpression.of(arg).toStringWithoutQuotes()); }
|
||||||
if ("initial".equals(methodName)) {
|
else if ("end".equals(methodName)) endStates.add(QuotedExpression.of(arg).toStringWithoutQuotes());
|
||||||
if (!isInsideRegionOrFork(call)) {
|
|
||||||
initialStates.add(QuotedExpression.of(arg).toStringWithoutQuotes());
|
|
||||||
}
|
|
||||||
} else if ("end".equals(methodName)) {
|
|
||||||
endStates.add(QuotedExpression.of(arg).toStringWithoutQuotes());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isInsideRegionOrFork(MethodInvocation mi) {
|
private boolean isInsideRegionOrFork(MethodInvocation mi) {
|
||||||
Expression expr = mi.getExpression();
|
Expression expr = mi.getExpression();
|
||||||
while (expr instanceof MethodInvocation parent) {
|
while (expr instanceof MethodInvocation parent) {
|
||||||
String methodName = parent.getName().getIdentifier();
|
if (List.of("fork", "region").contains(parent.getName().getIdentifier())) return true;
|
||||||
if (List.of("fork", "region").contains(methodName)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
expr = parent.getExpression();
|
expr = parent.getExpression();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MethodDeclaration findConfigureStatesMethod(TypeDeclaration td) {
|
private MethodDeclaration findConfigureStatesMethod(TypeDeclaration td) {
|
||||||
for (MethodDeclaration method : td.getMethods()) {
|
for (MethodDeclaration method : td.getMethods()) if (isConfigureStatesMethod(method)) return method;
|
||||||
if (isConfigureStatesMethod(method)) return method;
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isConfigureStatesMethod(MethodDeclaration method) {
|
private boolean isConfigureStatesMethod(MethodDeclaration method) {
|
||||||
if (!method.getName().getIdentifier().equals("configure")) return false;
|
if (!method.getName().getIdentifier().equals("configure") || method.parameters().size() != 1) return false;
|
||||||
List<?> params = method.parameters();
|
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().startsWith("StateMachineStateConfigurer");
|
||||||
if (params.size() != 1) return false;
|
|
||||||
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(0);
|
|
||||||
String typeName = param.getType().toString();
|
|
||||||
return typeName.startsWith("StateMachineStateConfigurer");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getSimpleName(Type type) {
|
private String getSimpleName(Type type) {
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ class RegressionTest {
|
|||||||
Path.of("../state_machines/inheritance_state_machine"),
|
Path.of("../state_machines/inheritance_state_machine"),
|
||||||
Path.of("src/test/resources/golden/OneStateMachineConfiguration"),
|
Path.of("src/test/resources/golden/OneStateMachineConfiguration"),
|
||||||
"OneStateMachineConfiguration"
|
"OneStateMachineConfiguration"
|
||||||
|
),
|
||||||
|
new TestScenario(
|
||||||
|
"Two State Machine (Extra Functions)",
|
||||||
|
Path.of("../state_machines/inheritance_extra_functions_state_machine"),
|
||||||
|
Path.of("src/test/resources/golden/TwoStateMachineExtraFunctionsConfiguration"),
|
||||||
|
"TwoStateMachineConfiguration"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -468,6 +468,25 @@ class AstTransitionParserTest {
|
|||||||
assertThat(transition.getIsLambdaActions()).containsExactly(true);
|
assertThat(transition.getIsLambdaActions()).containsExactly(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldParseTransitionsWithVariableDeclarations() {
|
||||||
|
String source = """
|
||||||
|
public class TestClass {
|
||||||
|
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
|
||||||
|
ExternalTransitionConfigurer<String, String> t1 = transitions.withExternal().source("S1").target("S2").event("E1");
|
||||||
|
t1.and().withExternal().source("S2").target("S3").event("E2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
MethodDeclaration method = createMethodDeclaration(source);
|
||||||
|
|
||||||
|
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
|
||||||
|
|
||||||
|
assertThat(transitions).hasSize(2);
|
||||||
|
assertThat(transitions.get(0).getEvent()).isEqualTo("E1");
|
||||||
|
assertThat(transitions.get(1).getEvent()).isEqualTo("E2");
|
||||||
|
}
|
||||||
|
|
||||||
private MethodDeclaration createMethodDeclaration(String source) {
|
private MethodDeclaration createMethodDeclaration(String source) {
|
||||||
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
||||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
||||||
@@ -483,4 +502,4 @@ class AstTransitionParserTest {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.app;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class StateMachineAggregatorTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
private CodebaseContext context;
|
||||||
|
private StateMachineAggregator aggregator;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
context = new CodebaseContext();
|
||||||
|
aggregator = new StateMachineAggregator(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAggregateTransitionsFromInheritance() throws IOException {
|
||||||
|
String baseSource = """
|
||||||
|
package com.example;
|
||||||
|
public abstract class BaseConfig extends StateMachineConfigurerAdapter {
|
||||||
|
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
|
||||||
|
transitions.withExternal().source("BASE_S1").target("BASE_S2").event("BASE_E1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
String childSource = """
|
||||||
|
package com.example;
|
||||||
|
public class ChildConfig extends BaseConfig {
|
||||||
|
@Override
|
||||||
|
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
|
||||||
|
super.configure(transitions);
|
||||||
|
transitions.withExternal().source("CHILD_S1").target("CHILD_S2").event("CHILD_E1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
writeClass("BaseConfig", baseSource);
|
||||||
|
writeClass("ChildConfig", childSource);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration childTd = context.getTypeDeclaration("ChildConfig");
|
||||||
|
List<Transition> transitions = aggregator.aggregateTransitions(childTd);
|
||||||
|
|
||||||
|
assertThat(transitions).hasSize(2);
|
||||||
|
assertThat(transitions).anyMatch(t -> t.getEvent().equals("BASE_E1"));
|
||||||
|
assertThat(transitions).anyMatch(t -> t.getEvent().equals("CHILD_E1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFollowMethodCallsAcrossHierarchy() throws IOException {
|
||||||
|
String baseSource = """
|
||||||
|
package com.example;
|
||||||
|
public abstract class BaseConfig extends StateMachineConfigurerAdapter {
|
||||||
|
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
|
||||||
|
addBaseTransitions(transitions);
|
||||||
|
}
|
||||||
|
protected void addBaseTransitions(StateMachineTransitionConfigurer transitions) {
|
||||||
|
transitions.withExternal().source("BASE_S1").target("BASE_S2").event("BASE_E1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
String childSource = """
|
||||||
|
package com.example;
|
||||||
|
public class ChildConfig extends BaseConfig {
|
||||||
|
@Override
|
||||||
|
protected void addBaseTransitions(StateMachineTransitionConfigurer transitions) {
|
||||||
|
transitions.withExternal().source("OVERRIDDEN_S1").target("OVERRIDDEN_S2").event("OVERRIDDEN_E1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
writeClass("BaseConfig", baseSource);
|
||||||
|
writeClass("ChildConfig", childSource);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration childTd = context.getTypeDeclaration("ChildConfig");
|
||||||
|
List<Transition> transitions = aggregator.aggregateTransitions(childTd);
|
||||||
|
|
||||||
|
assertThat(transitions).hasSize(1);
|
||||||
|
assertThat(transitions.get(0).getEvent()).isEqualTo("OVERRIDDEN_E1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveParametersInMethodCalls() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class ParamConfig extends StateMachineConfigurerAdapter {
|
||||||
|
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
|
||||||
|
addCustomTransition(transitions, "START", "END", "GO");
|
||||||
|
}
|
||||||
|
private void addCustomTransition(StateMachineTransitionConfigurer t, String src, String tgt, String evt) {
|
||||||
|
t.withExternal().source(src).target(tgt).event(evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
writeClass("ParamConfig", source);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("ParamConfig");
|
||||||
|
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
||||||
|
|
||||||
|
assertThat(transitions).hasSize(1);
|
||||||
|
assertThat(transitions.get(0).getSourceStates()).containsExactly("START");
|
||||||
|
assertThat(transitions.get(0).getTargetStates()).containsExactly("END");
|
||||||
|
assertThat(transitions.get(0).getEvent()).isEqualTo("GO");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldParseStreamOfForEachTransitions() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
public class StreamConfig extends StateMachineConfigurerAdapter {
|
||||||
|
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
|
||||||
|
Stream.of("E1", "E2").forEach(e -> {
|
||||||
|
transitions.withExternal().source("S1").target("S2").event(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
writeClass("StreamConfig", source);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("StreamConfig");
|
||||||
|
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
||||||
|
|
||||||
|
assertThat(transitions).hasSize(2);
|
||||||
|
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E1"));
|
||||||
|
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAggregateStatesAcrossInheritance() throws IOException {
|
||||||
|
String baseSource = """
|
||||||
|
package com.example;
|
||||||
|
public abstract class BaseConfig extends StateMachineConfigurerAdapter {
|
||||||
|
public void configure(StateMachineStateConfigurer states) throws Exception {
|
||||||
|
states.withStates().initial("BASE_START");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
String childSource = """
|
||||||
|
package com.example;
|
||||||
|
public class ChildConfig extends BaseConfig {
|
||||||
|
public void configure(StateMachineStateConfigurer states) throws Exception {
|
||||||
|
super.configure(states);
|
||||||
|
states.withStates().state("CHILD_STATE").end("CHILD_END");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
writeClass("BaseConfig", baseSource);
|
||||||
|
writeClass("ChildConfig", childSource);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration childTd = context.getTypeDeclaration("ChildConfig");
|
||||||
|
Set<String> initialStates = new HashSet<>();
|
||||||
|
Set<String> endStates = new HashSet<>();
|
||||||
|
aggregator.aggregateStates(childTd, initialStates, endStates);
|
||||||
|
|
||||||
|
assertThat(initialStates).containsExactly("BASE_START");
|
||||||
|
assertThat(endStates).containsExactly("CHILD_END");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldParseStreamOfForEachStates() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
public class StreamStateConfig extends StateMachineConfigurerAdapter {
|
||||||
|
public void configure(StateMachineStateConfigurer states) throws Exception {
|
||||||
|
Stream.of("S1", "S2").forEach(s -> {
|
||||||
|
states.withStates().initial(s);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
writeClass("StreamStateConfig", source);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("StreamStateConfig");
|
||||||
|
Set<String> initialStates = new HashSet<>();
|
||||||
|
Set<String> endStates = new HashSet<>();
|
||||||
|
aggregator.aggregateStates(td, initialStates, endStates);
|
||||||
|
|
||||||
|
assertThat(initialStates).containsExactlyInAnyOrder("S1", "S2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAggregateTransitionsFromDeepInheritance() throws IOException {
|
||||||
|
String grandparentSource = """
|
||||||
|
package com.example;
|
||||||
|
public abstract class GrandparentConfig extends StateMachineConfigurerAdapter {
|
||||||
|
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
|
||||||
|
transitions.withExternal().source("GP_S1").target("GP_S2").event("GP_E1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
String parentSource = """
|
||||||
|
package com.example;
|
||||||
|
public abstract class ParentConfig extends GrandparentConfig {
|
||||||
|
@Override
|
||||||
|
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
|
||||||
|
super.configure(transitions);
|
||||||
|
transitions.withExternal().source("P_S1").target("P_S2").event("P_E1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
String childSource = """
|
||||||
|
package com.example;
|
||||||
|
public class ChildConfig extends ParentConfig {
|
||||||
|
@Override
|
||||||
|
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
|
||||||
|
super.configure(transitions);
|
||||||
|
transitions.withExternal().source("C_S1").target("C_S2").event("C_E1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
writeClass("GrandparentConfig", grandparentSource);
|
||||||
|
writeClass("ParentConfig", parentSource);
|
||||||
|
writeClass("ChildConfig", childSource);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration childTd = context.getTypeDeclaration("ChildConfig");
|
||||||
|
List<Transition> transitions = aggregator.aggregateTransitions(childTd);
|
||||||
|
|
||||||
|
assertThat(transitions).hasSize(3);
|
||||||
|
assertThat(transitions).anyMatch(t -> t.getEvent().equals("GP_E1"));
|
||||||
|
assertThat(transitions).anyMatch(t -> t.getEvent().equals("P_E1"));
|
||||||
|
assertThat(transitions).anyMatch(t -> t.getEvent().equals("C_E1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldParseEnhancedForLoopTransitions() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
import java.util.List;
|
||||||
|
public class LoopConfig extends StateMachineConfigurerAdapter {
|
||||||
|
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
|
||||||
|
for (String e : List.of("E1", "E2")) {
|
||||||
|
transitions.withExternal().source("S1").target("S2").event(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
writeClass("LoopConfig", source);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("LoopConfig");
|
||||||
|
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
||||||
|
|
||||||
|
assertThat(transitions).hasSize(2);
|
||||||
|
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E1"));
|
||||||
|
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeClass(String name, String source) throws IOException {
|
||||||
|
Files.writeString(tempDir.resolve(name + ".java"), source);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import org.springframework.statemachine.config.configurers.ExternalTransitionCon
|
|||||||
import org.springframework.statemachine.config.configurers.StateConfigurer;
|
import org.springframework.statemachine.config.configurers.StateConfigurer;
|
||||||
import org.springframework.statemachine.guard.Guard;
|
import org.springframework.statemachine.guard.Guard;
|
||||||
|
|
||||||
abstract class AbstractOneStateMachineConfiguration extends AbstractStateMachineConfiguration<States, Events> {
|
abstract class AbstractTwoStateMachineConfiguration extends AbstractStateMachineConfiguration<States, Events> {
|
||||||
|
|
||||||
protected abstract void addAdditionalStates(StateConfigurer<States, Events> states);
|
protected abstract void addAdditionalStates(StateConfigurer<States, Events> states);
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ import org.springframework.statemachine.config.builders.StateMachineTransitionCo
|
|||||||
import org.springframework.statemachine.config.configurers.ExternalTransitionConfigurer;
|
import org.springframework.statemachine.config.configurers.ExternalTransitionConfigurer;
|
||||||
import org.springframework.statemachine.config.configurers.StateConfigurer;
|
import org.springframework.statemachine.config.configurers.StateConfigurer;
|
||||||
|
|
||||||
class OneStateMachineConfiguration extends AbstractOneStateMachineConfiguration {
|
class TwoStateMachineConfiguration extends AbstractTwoStateMachineConfiguration {
|
||||||
@Override
|
@Override
|
||||||
protected void addAdditionalStates(StateConfigurer<States, Events> states) {
|
protected void addAdditionalStates(StateConfigurer<States, Events> states) {
|
||||||
states.state(States.STATE_EXTRA_1);
|
states.state(States.STATE_EXTRA_1);
|
||||||
Reference in New Issue
Block a user