Prune switch-dispatch call paths and scope analysis per state machine.

Switch-case constraints on call-graph edges and forward path binding stop string-key endpoints from linking every dispatch arm; machine-scoped triggers/call chains reduce cross-config noise, with pipeline bug fixes and regression coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-11 20:49:16 +02:00
parent 8fa6a44e75
commit 8a97bceca4
24 changed files with 1208 additions and 1218 deletions

View File

@@ -18,8 +18,9 @@ public class CallChainEnricher implements AnalysisEnricher {
List<CallChain> chains = intelligence.findCallChains( List<CallChain> chains = intelligence.findCallChains(
result.getMetadata().getEntryPoints(), result.getMetadata().getEntryPoints(),
result.getMetadata().getTriggers() intelligence.findTriggerPoints()
); );
chains = MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
result.addMetadata(CodebaseMetadata.builder() result.addMetadata(CodebaseMetadata.builder()
.callChains(chains) .callChains(chains)

View File

@@ -0,0 +1,57 @@
package click.kamil.springstatemachineexporter.analysis.enricher;
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import java.util.ArrayList;
import java.util.List;
/**
* Filters codebase-wide analysis artifacts to those relevant for a single state machine config.
*/
public final class MachineScopeFilter {
private static final BeanResolutionEngine ROUTING = new HeuristicBeanResolutionEngine();
private MachineScopeFilter() {
}
public static List<TriggerPoint> filterTriggersForMachine(
List<TriggerPoint> triggers, String machineName, CodebaseContext context) {
if (triggers == null || machineName == null) {
return triggers == null ? List.of() : triggers;
}
List<TriggerPoint> filtered = new ArrayList<>();
for (TriggerPoint trigger : triggers) {
if (isTriggerForMachine(trigger, machineName, context)) {
filtered.add(trigger);
}
}
return filtered;
}
public static List<CallChain> filterCallChainsForMachine(
List<CallChain> chains, String machineName, CodebaseContext context) {
if (chains == null || machineName == null) {
return chains == null ? List.of() : chains;
}
List<CallChain> filtered = new ArrayList<>();
for (CallChain chain : chains) {
if (ROUTING.isRoutedToCorrectMachine(chain, machineName, context)) {
filtered.add(chain);
}
}
return filtered;
}
private static boolean isTriggerForMachine(TriggerPoint trigger, String machineName, CodebaseContext context) {
if (trigger == null) {
return false;
}
CallChain probe = CallChain.builder().triggerPoint(trigger).methodChain(List.of()).build();
return ROUTING.isRoutedToCorrectMachine(probe, machineName, context);
}
}

View File

@@ -93,9 +93,11 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
} }
} }
// Update the metadata with the new call chains List<TriggerPoint> scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
result.getMetadata().getTriggers(), result.getName(), context);
CodebaseMetadata updatedMetadata = CodebaseMetadata.builder() CodebaseMetadata updatedMetadata = CodebaseMetadata.builder()
.triggers(result.getMetadata().getTriggers()) .triggers(scopedTriggers)
.entryPoints(result.getMetadata().getEntryPoints()) .entryPoints(result.getMetadata().getEntryPoints())
.callChains(updatedChains) .callChains(updatedChains)
.properties(result.getMetadata().getProperties()) .properties(result.getMetadata().getProperties())

View File

@@ -16,9 +16,8 @@ public class TriggerEnricher implements AnalysisEnricher {
log.info("Enriching {} with triggers", result.getName()); log.info("Enriching {} with triggers", result.getName());
List<TriggerPoint> triggers = intelligence.findTriggerPoints(); List<TriggerPoint> triggers = intelligence.findTriggerPoints();
triggers = MachineScopeFilter.filterTriggersForMachine(triggers, result.getName(), context);
// Initially, we just add all triggers found in the codebase to the metadata.
// In later steps of Phase 2, we will filter them by State Machine ID.
result.addMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder() result.addMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
.triggers(triggers) .triggers(triggers)
.build()); .build());

View File

@@ -219,6 +219,18 @@ public final class AccessorResolver {
if (!cicValues.isEmpty()) { if (!cicValues.isEmpty()) {
return cicValues; return cicValues;
} }
} else if (receiverContext != null && receiverContext.cic() != null) {
ReceiverContext enriched = new ReceiverContext(
context.getTypeDeclaration(ownerFqn),
receiverContext.cic(),
receiverContext.contextCu(),
receiverContext.anonymousGetter(),
receiverContext.fieldValues());
List<String> cicValues = resolveWithCic(
ownerFqn, methodName, enriched, indexedAccessor.orElse(null), visited);
if (!cicValues.isEmpty()) {
return cicValues;
}
} }
if (constantExtractor != null && !budget.isMethodReturnExhausted(0)) { if (constantExtractor != null && !budget.isMethodReturnExhausted(0)) {
@@ -258,7 +270,6 @@ public final class AccessorResolver {
} }
if (receiverContext != null if (receiverContext != null
&& receiverContext.cic() != null
&& receiverContext.fieldValues() != null && receiverContext.fieldValues() != null
&& accessor.fieldName() != null) { && accessor.fieldName() != null) {
String fieldValue = receiverContext.fieldValues().get(accessor.fieldName()); String fieldValue = receiverContext.fieldValues().get(accessor.fieldName());
@@ -276,6 +287,9 @@ public final class AccessorResolver {
AccessorSummary indexedAccessor, AccessorSummary indexedAccessor,
Set<String> visited) { Set<String> visited) {
TypeDeclaration td = receiverContext.ownerType(); TypeDeclaration td = receiverContext.ownerType();
if (td == null) {
td = context.getTypeDeclaration(ownerFqn);
}
ClassInstanceCreation cic = receiverContext.cic(); ClassInstanceCreation cic = receiverContext.cic();
Map<String, String> fieldValues = receiverContext.fieldValues() != null Map<String, String> fieldValues = receiverContext.fieldValues() != null
? receiverContext.fieldValues() ? receiverContext.fieldValues()
@@ -400,7 +414,10 @@ public final class AccessorResolver {
String cicKey = receiverContext != null && receiverContext.cic() != null String cicKey = receiverContext != null && receiverContext.cic() != null
? receiverContext.cic().toString() ? receiverContext.cic().toString()
: ""; : "";
return ownerFqn + "#" + methodName + "#" + cicKey; String fieldValuesKey = receiverContext != null && receiverContext.fieldValues() != null
? receiverContext.fieldValues().toString()
: "";
return ownerFqn + "#" + methodName + "#" + cicKey + "#" + fieldValuesKey;
} }
private static String simplifyTypeName(String typeFqn) { private static String simplifyTypeName(String typeFqn) {

View File

@@ -1,7 +1,9 @@
package click.kamil.springstatemachineexporter.analysis.resolver; package click.kamil.springstatemachineexporter.analysis.resolver;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -60,6 +62,66 @@ public final class BooleanConstraintEvaluator {
} }
} }
/**
* Evaluates a constraint against known variable bindings gathered while walking a call path forward.
* Supports {@code var == VALUE} where VALUE may be a quoted string or enum constant; suffix matching
* is used so {@code command == ORDER_PAY} matches {@code DomainCommand.ORDER_PAY}.
*/
public static boolean isCompatibleWithBindings(String constraint, Map<String, String> bindings) {
if (constraint == null || constraint.isBlank()) {
return true;
}
if (bindings == null || bindings.isEmpty()) {
return true;
}
for (String varName : extractEqualityVariables(constraint)) {
if (!bindings.containsKey(varName) || varName.equals(bindings.get(varName))) {
return true;
}
}
String expr = constraint;
for (Map.Entry<String, String> entry : bindings.entrySet()) {
expr = substituteVariableBindings(expr, entry.getKey(), entry.getValue());
}
return evaluateBooleanExpression(expr);
}
private static Set<String> extractEqualityVariables(String constraint) {
Set<String> vars = new HashSet<>();
Pattern pattern = Pattern.compile("([a-zA-Z][\\w]*)\\s*==");
Matcher matcher = pattern.matcher(constraint);
while (matcher.find()) {
vars.add(matcher.group(1));
}
return vars;
}
private static String substituteVariableBindings(String expr, String varName, String boundValue) {
if (boundValue == null || boundValue.isBlank()) {
return expr;
}
String cleanValue = boundValue;
if (cleanValue.startsWith("\"") && cleanValue.endsWith("\"")) {
cleanValue = cleanValue.substring(1, cleanValue.length() - 1);
}
String suffix = cleanValue.contains(".") ? cleanValue.substring(cleanValue.lastIndexOf('.') + 1) : cleanValue;
Pattern eqPattern = Pattern.compile(
"(?i)" + Pattern.quote(varName) + "\\s*==\\s*([\\w.\"']+)");
Matcher matcher = eqPattern.matcher(expr);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String rhs = matcher.group(1).replace("\"", "").replace("'", "");
boolean matches = cleanValue.equals(rhs)
|| cleanValue.endsWith("." + rhs)
|| suffix.equalsIgnoreCase(rhs)
|| cleanValue.equalsIgnoreCase(rhs);
matcher.appendReplacement(sb, matches ? "true" : "false");
}
matcher.appendTail(sb);
return sb.toString();
}
/** /**
* Returns {@code true}/{@code false} when both sides are compile-time string literals; otherwise {@code null}. * Returns {@code true}/{@code false} when both sides are compile-time string literals; otherwise {@code null}.
*/ */

View File

@@ -29,6 +29,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
protected final ExpressionAccessClassifier expressionAccessClassifier; protected final ExpressionAccessClassifier expressionAccessClassifier;
protected final AccessorResolver accessorResolver; protected final AccessorResolver accessorResolver;
protected final FieldInitializerFinder fieldInitializerFinder; protected final FieldInitializerFinder fieldInitializerFinder;
protected final PathBindingEvaluator pathBindingEvaluator;
private final Map<String, ASTNode> parsedNodeCache = new HashMap<>(); private final Map<String, ASTNode> parsedNodeCache = new HashMap<>();
private ASTNode parseExpressionString(String expr) { private ASTNode parseExpressionString(String expr) {
@@ -91,6 +92,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
this.accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver); this.accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
this.constantExtractor.setAccessorResolver(this.accessorResolver); this.constantExtractor.setAccessorResolver(this.accessorResolver);
this.fieldInitializerFinder = new FieldInitializerFinder(context); this.fieldInitializerFinder = new FieldInitializerFinder(context);
this.pathBindingEvaluator = new PathBindingEvaluator(context, variableTracer, constantResolver, typeResolver);
} }
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) { public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
@@ -111,6 +113,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
} }
Set<List<String>> uniquePaths = new LinkedHashSet<>(allPaths); Set<List<String>> uniquePaths = new LinkedHashSet<>(allPaths);
for (List<String> path : uniquePaths) { for (List<String> path : uniquePaths) {
if (!pathBindingEvaluator.isPathCompatible(path, callGraph, pathFinder)) {
continue;
}
foundAny = true; foundAny = true;
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph); TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
if (resolvedTp != null) { if (resolvedTp != null) {
@@ -1281,25 +1286,17 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
if (currentTd != null) { if (currentTd != null) {
String receiverType = context.getSuperclassFqn(currentTd); String receiverType = context.getSuperclassFqn(currentTd);
if (receiverType != null) { if (receiverType != null) {
Optional<AccessorSummary> indexedSuperGetter = CompilationUnit contextCu = currentTd.getRoot() instanceof CompilationUnit cu ? cu : null;
context.getAccessorIndex().lookup(receiverType, getterName); List<String> resolved = accessorResolver.resolve(
if (indexedSuperGetter.isPresent() && indexedSuperGetter.get().isGetter()) { receiverType,
CompilationUnit contextCu = currentTd.getRoot() instanceof CompilationUnit cu ? cu : null; getterName,
if (indexedSuperGetter.get().kind() new AccessorResolver.ReceiverContext(
== click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER) { context.getTypeDeclaration(receiverType), null, contextCu, false, null),
List<String> constants = constantExtractor.resolveMethodReturnConstant( ResolutionBudget.defaults());
receiverType, getterName, 0, new HashSet<>(), contextCu); if (!resolved.isEmpty()) {
if (!constants.isEmpty()) { return resolved;
return constants;
}
}
List<String> fieldConstants = constantExtractor.resolveIndexedGetterFieldConstants(
indexedSuperGetter.get(), contextCu, new HashSet<>());
if (!fieldConstants.isEmpty()) {
return fieldConstants;
}
} }
return constantExtractor.resolveMethodReturnConstant(receiverType, getterName, 0, new HashSet<>(), null); return constantExtractor.resolveMethodReturnConstant(receiverType, getterName, 0, new HashSet<>(), contextCu);
} }
} }
} else if (exprNode instanceof MethodInvocation mi) { } else if (exprNode instanceof MethodInvocation mi) {
@@ -1315,7 +1312,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
receiverType = variableTracer.getVariableDeclaredType(scopeMethod, receiverName); receiverType = variableTracer.getVariableDeclaredType(scopeMethod, receiverName);
} else if (receiver instanceof ClassInstanceCreation cic) { } else if (receiver instanceof ClassInstanceCreation cic) {
directCic = cic; directCic = cic;
receiverType = cic.getType().toString(); receiverType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
} else if (receiver instanceof MethodInvocation receiverMi) { } else if (receiver instanceof MethodInvocation receiverMi) {
GetterChainReceiver chainedReceiver = resolveGetterChainToReceiver(receiverMi, scopeMethod); GetterChainReceiver chainedReceiver = resolveGetterChainToReceiver(receiverMi, scopeMethod);
if (chainedReceiver != null) { if (chainedReceiver != null) {

View File

@@ -419,7 +419,8 @@ public class ConstantExtractor {
return super.visit(node); return super.visit(node);
} }
}); });
} else if (widenToImplementations) { }
if (constants.isEmpty() && widenToImplementations) {
List<String> impls = context.getImplementations(className); List<String> impls = context.getImplementations(className);
if (impls != null) { if (impls != null) {
for (String implName : impls) { for (String implName : impls) {

View File

@@ -170,13 +170,15 @@ public class ConstructorAnalyzer {
} }
if (results.isEmpty()) { if (results.isEmpty()) {
// Context-Aware Fallback (Down): Check subclasses/implementations // Context-Aware Fallback (Down): widen only for interface/abstract types
List<String> impls = context.getImplementations(fqn); if (shouldWidenToImplementations(td)) {
if (impls != null) { List<String> impls = context.getImplementations(fqn);
for (String implFqn : impls) { if (impls != null) {
TypeDeclaration implTd = context.getTypeDeclaration(implFqn); for (String implFqn : impls) {
if (implTd != null) { TypeDeclaration implTd = context.getTypeDeclaration(implFqn);
results.addAll(traceFieldInConstructors(implTd, fieldName, context, visited, budget)); if (implTd != null) {
results.addAll(traceFieldInConstructors(implTd, fieldName, context, visited, budget));
}
} }
} }
} }
@@ -187,10 +189,11 @@ public class ConstructorAnalyzer {
Collection<TypeDeclaration> allTypes = context.getTypeDeclarations(); Collection<TypeDeclaration> allTypes = context.getTypeDeclarations();
if (allTypes != null) { if (allTypes != null) {
int matchedCount = 0; int matchedCount = 0;
ResolutionBudget localBudget = ResolutionBudget.defaults();
for (TypeDeclaration typeDecl : allTypes) { for (TypeDeclaration typeDecl : allTypes) {
if (typeDecl == null) continue; if (typeDecl == null) continue;
if (context.hasField(typeDecl, fieldName)) { if (context.hasField(typeDecl, fieldName)) {
results.addAll(traceFieldInConstructors(typeDecl, fieldName, context, visited, budget)); results.addAll(traceFieldInConstructors(typeDecl, fieldName, context, visited, localBudget));
matchedCount++; matchedCount++;
if (matchedCount >= budget.globalFieldScanLimit()) { if (matchedCount >= budget.globalFieldScanLimit()) {
break; break;
@@ -204,6 +207,13 @@ public class ConstructorAnalyzer {
return results; return results;
} }
private static boolean shouldWidenToImplementations(TypeDeclaration td) {
if (td == null) {
return true;
}
return td.isInterface() || Modifier.isAbstract(td.getModifiers());
}
public Map<String, String> buildFieldValuesFromCIC( public Map<String, String> buildFieldValuesFromCIC(
TypeDeclaration td, TypeDeclaration td,
ClassInstanceCreation cic, ClassInstanceCreation cic,

View File

@@ -31,6 +31,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
private final SpringComponentDetector componentDetector; private final SpringComponentDetector componentDetector;
private List<TriggerPoint> cachedTriggers; private List<TriggerPoint> cachedTriggers;
private List<EntryPoint> cachedEntryPoints; private List<EntryPoint> cachedEntryPoints;
private List<CallChain> cachedCallChains;
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) { public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
this.context = context; this.context = context;
@@ -93,8 +94,12 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
@Override @Override
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) { public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
if (cachedCallChains != null) {
return cachedCallChains;
}
log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers", entryPoints.size(), triggers.size()); log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers", entryPoints.size(), triggers.size());
return callGraphEngine.findChains(entryPoints, triggers); cachedCallChains = callGraphEngine.findChains(entryPoints, triggers);
return cachedCallChains;
} }
@Override @Override

View File

@@ -0,0 +1,329 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Walks a call path forward, propagating known parameter / local bindings so switch-case
* edge constraints can prune impossible branches (e.g. {@code dispatch} arms).
*/
public class PathBindingEvaluator {
private final CodebaseContext context;
private final VariableTracer variableTracer;
private final ConstantResolver constantResolver;
private final TypeResolver typeResolver;
public PathBindingEvaluator(
CodebaseContext context,
VariableTracer variableTracer,
ConstantResolver constantResolver,
TypeResolver typeResolver) {
this.context = context;
this.variableTracer = variableTracer;
this.constantResolver = constantResolver;
this.typeResolver = typeResolver;
}
/** Visible for tests: bindings accumulated while walking a path forward (ignores constraint rejection). */
Map<String, String> traceBindings(List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
Map<String, String> bindings = new HashMap<>();
if (path == null || path.size() < 2) {
return bindings;
}
for (int i = 0; i < path.size() - 1; i++) {
String caller = path.get(i);
String target = path.get(i + 1);
CallEdge edge = findEdge(caller, target, callGraph, pathFinder);
if (edge == null) {
continue;
}
enrichBindings(caller, target, edge, path, i + 1, callGraph, pathFinder, bindings);
}
return bindings;
}
public boolean isPathCompatible(List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
if (path == null || path.size() < 2) {
return true;
}
Map<String, String> bindings = new HashMap<>();
for (int i = 0; i < path.size() - 1; i++) {
String caller = path.get(i);
String target = path.get(i + 1);
CallEdge edge = findEdge(caller, target, callGraph, pathFinder);
if (edge == null) {
continue;
}
if (edge.getConstraint() != null
&& !edge.getConstraint().isBlank()
&& !BooleanConstraintEvaluator.isCompatibleWithBindings(edge.getConstraint(), bindings)) {
return false;
}
enrichBindings(caller, target, edge, path, i + 1, callGraph, pathFinder, bindings);
}
return true;
}
private void enrichBindings(
String caller,
String target,
CallEdge edge,
List<String> path,
int pathIndex,
Map<String, List<CallEdge>> callGraph,
CallGraphPathFinder pathFinder,
Map<String, String> bindings) {
Map<String, String> paramValues = variableTracer.buildParameterValuesMap(caller, target, callGraph, path, pathIndex);
mergeResolvedBindings(bindings, paramValues, caller);
List<String> targetParams = typeResolver.getParameterNames(target);
if (targetParams == null || edge.getArguments() == null) {
return;
}
for (int i = 0; i < edge.getArguments().size() && i < targetParams.size(); i++) {
String arg = edge.getArguments().get(i);
String paramName = targetParams.get(i);
String resolved = resolveBindingValue(caller, arg, bindings);
if (shouldStoreBinding(paramName, resolved)) {
bindings.put(paramName, resolved);
}
}
}
private boolean shouldStoreBinding(String paramName, String resolved) {
if (resolved == null || resolved.isBlank() || resolved.equals(paramName)) {
return false;
}
if (resolved.contains("(") && !isEnumLikeConstant(resolved)) {
return false;
}
return true;
}
private void mergeResolvedBindings(Map<String, String> bindings, Map<String, String> paramValues, String caller) {
if (paramValues == null || paramValues.isEmpty()) {
return;
}
for (Map.Entry<String, String> entry : paramValues.entrySet()) {
String resolved = resolveBindingValue(caller, entry.getValue(), bindings);
if (shouldStoreBinding(entry.getKey(), resolved)) {
bindings.put(entry.getKey(), resolved);
}
}
}
private String resolveBindingValue(String callerFqn, String rawValue, Map<String, String> bindings) {
if (rawValue == null || rawValue.isBlank()) {
return null;
}
if (rawValue.startsWith("\"") || isEnumLikeConstant(rawValue)) {
return normalizeLiteral(rawValue);
}
if (bindings.containsKey(rawValue)) {
return bindings.get(rawValue);
}
String traced = variableTracer.traceLocalVariable(callerFqn, rawValue, bindings);
if (traced != null && !traced.isBlank()) {
if (traced.startsWith("\"") || isEnumLikeConstant(traced)) {
return traced;
}
String fromCall = resolveMethodCallFromSource(callerFqn, rawValue, bindings);
if (fromCall != null) {
return fromCall;
}
if (!traced.equals(rawValue)) {
return traced;
}
}
return rawValue;
}
private String resolveMethodCallFromSource(String callerFqn, String varName, Map<String, String> bindings) {
MethodDeclaration md = findMethodDeclaration(callerFqn);
if (md == null || md.getBody() == null) {
return null;
}
final Expression[] initializer = new Expression[1];
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
initializer[0] = node.getInitializer();
}
return super.visit(node);
}
});
if (initializer[0] == null) {
return null;
}
return evaluateExpressionValue(initializer[0], callerFqn, bindings);
}
private String evaluateExpressionValue(Expression expr, String callerFqn, Map<String, String> bindings) {
if (expr instanceof StringLiteral sl) {
return "\"" + sl.getLiteralValue() + "\"";
}
if (expr instanceof QualifiedName qn) {
return qn.getFullyQualifiedName();
}
if (expr instanceof SimpleName sn) {
return bindings.getOrDefault(sn.getIdentifier(), sn.getIdentifier());
}
if (expr instanceof MethodInvocation mi) {
return evaluateMethodInvocationReturn(mi, callerFqn, bindings);
}
if (expr instanceof SwitchExpression se) {
return constantResolver.evaluateSwitchWithParams(se, bindings, context);
}
String resolved = constantResolver.resolve(expr, context);
return resolved;
}
private String evaluateMethodInvocationReturn(MethodInvocation mi, String callerFqn, Map<String, String> bindings) {
String ownerFqn = resolveMethodOwnerFqn(mi, callerFqn);
if (ownerFqn == null) {
return null;
}
String methodName = mi.getName().getIdentifier();
TypeDeclaration owner = context.getTypeDeclaration(ownerFqn);
if (owner == null) {
return null;
}
MethodDeclaration md = context.findMethodDeclaration(owner, methodName, true);
if (md == null || md.getBody() == null) {
return null;
}
Map<String, String> callBindings = new HashMap<>(bindings);
for (int i = 0; i < md.parameters().size() && i < mi.arguments().size(); i++) {
SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(i);
String paramName = param.getName().getIdentifier();
String argValue = evaluateExpressionValue((Expression) mi.arguments().get(i), callerFqn, callBindings);
if (argValue != null) {
callBindings.put(paramName, argValue);
}
}
final String[] result = new String[1];
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement node) {
if (node.getExpression() instanceof SwitchExpression se) {
result[0] = constantResolver.evaluateSwitchWithParams(se, callBindings, context);
} else if (node.getExpression() != null) {
result[0] = evaluateExpressionValue(node.getExpression(), ownerFqn + "." + methodName, callBindings);
}
return super.visit(node);
}
});
return result[0];
}
private String resolveFieldTypeFqn(String callerFqn, String fieldName) {
if (callerFqn == null || !callerFqn.contains(".")) {
return null;
}
String className = callerFqn.substring(0, callerFqn.lastIndexOf('.'));
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) {
return null;
}
for (FieldDeclaration field : td.getFields()) {
for (Object fragmentObj : field.fragments()) {
VariableDeclarationFragment fragment = (VariableDeclarationFragment) fragmentObj;
if (fragment.getName().getIdentifier().equals(fieldName)) {
IVariableBinding binding = fragment.resolveBinding();
if (binding != null && binding.getType() != null) {
return binding.getType().getErasure().getQualifiedName();
}
String simpleType = field.getType().toString();
TypeDeclaration resolved = context.getTypeDeclaration(simpleType);
if (resolved != null) {
return context.getFqn(resolved);
}
if (td.getRoot() instanceof CompilationUnit cu && cu.getPackage() != null) {
String pkg = cu.getPackage().getName().getFullyQualifiedName();
resolved = context.getTypeDeclaration(pkg + "." + simpleType);
if (resolved != null) {
return context.getFqn(resolved);
}
}
return simpleType;
}
}
}
return null;
}
private String resolveMethodOwnerFqn(MethodInvocation mi, String callerFqn) {
IMethodBinding methodBinding = mi.resolveMethodBinding();
if (methodBinding != null && methodBinding.getDeclaringClass() != null) {
return methodBinding.getDeclaringClass().getErasure().getQualifiedName();
}
Expression receiver = mi.getExpression();
if (receiver instanceof SimpleName sn) {
return resolveFieldTypeFqn(callerFqn, sn.getIdentifier());
}
if (receiver instanceof FieldAccess fa) {
IVariableBinding fieldBinding = fa.resolveFieldBinding();
if (fieldBinding != null && fieldBinding.getType() != null) {
return fieldBinding.getType().getErasure().getQualifiedName();
}
return resolveFieldTypeFqn(callerFqn, fa.getName().getIdentifier());
}
return null;
}
private MethodDeclaration findMethodDeclaration(String methodFqn) {
if (methodFqn == null || !methodFqn.contains(".")) {
return null;
}
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) {
return null;
}
return context.findMethodDeclaration(td, methodName, true);
}
private static String normalizeLiteral(String value) {
if (value != null && value.startsWith("\"") && value.endsWith("\"")) {
return value.substring(1, value.length() - 1);
}
return value;
}
private static boolean isEnumLikeConstant(String value) {
if (value == null || value.isEmpty()) {
return false;
}
int dot = value.lastIndexOf('.');
if (dot <= 0 || dot >= value.length() - 1) {
return false;
}
return Character.isUpperCase(value.charAt(dot + 1));
}
private static CallEdge findEdge(
String caller, String target, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
List<CallEdge> edges = callGraph.get(caller);
if (edges == null) {
return null;
}
for (CallEdge edge : edges) {
if (edge.getTargetMethod().equals(target) || pathFinder.isHeuristicMatch(edge.getTargetMethod(), target)) {
return edge;
}
}
return null;
}
}

View File

@@ -30,6 +30,28 @@ public class TypeResolver {
return -1; return -1;
} }
public List<String> getParameterNames(String methodFqn) {
if (methodFqn == null || !methodFqn.contains(".")) {
return null;
}
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) {
return null;
}
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md == null) {
return null;
}
List<String> paramNames = new ArrayList<>();
for (Object paramObj : md.parameters()) {
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
paramNames.add(param.getName().getIdentifier());
}
return paramNames;
}
public String getParameterName(String methodFqn, int index) { public String getParameterName(String methodFqn, int index) {
if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null; if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));

View File

@@ -113,8 +113,100 @@ public final class AstUtils {
} }
current = parent; current = parent;
} }
String switchConstraint = buildSwitchCaseConstraint(findEnclosingSwitchCase(node));
if (switchConstraint != null) {
conditions.add(switchConstraint);
}
if (conditions.isEmpty()) return null; if (conditions.isEmpty()) return null;
java.util.Collections.reverse(conditions); java.util.Collections.reverse(conditions);
return String.join(" && ", conditions); return String.join(" && ", conditions);
} }
static org.eclipse.jdt.core.dom.SwitchCase findEnclosingSwitchCase(org.eclipse.jdt.core.dom.ASTNode node) {
org.eclipse.jdt.core.dom.ASTNode current = node;
while (current != null && !(current instanceof org.eclipse.jdt.core.dom.MethodDeclaration)) {
if (current instanceof org.eclipse.jdt.core.dom.SwitchCase switchCase) {
return switchCase;
}
current = current.getParent();
}
current = node;
while (current != null && !(current instanceof org.eclipse.jdt.core.dom.MethodDeclaration)) {
if (current instanceof org.eclipse.jdt.core.dom.YieldStatement yieldStatement) {
org.eclipse.jdt.core.dom.ASTNode parent = yieldStatement.getParent();
if (parent instanceof org.eclipse.jdt.core.dom.SwitchStatement switchStatement) {
org.eclipse.jdt.core.dom.SwitchCase switchCase =
findSwitchCaseForStatement(switchStatement.statements(), yieldStatement);
if (switchCase != null) {
return switchCase;
}
} else if (parent instanceof org.eclipse.jdt.core.dom.SwitchExpression switchExpression) {
org.eclipse.jdt.core.dom.SwitchCase switchCase =
findSwitchCaseForStatement(switchExpression.statements(), yieldStatement);
if (switchCase != null) {
return switchCase;
}
}
} else if (current instanceof org.eclipse.jdt.core.dom.ExpressionStatement expressionStatement) {
org.eclipse.jdt.core.dom.ASTNode parent = expressionStatement.getParent();
if (parent instanceof org.eclipse.jdt.core.dom.SwitchCase switchCase) {
return switchCase;
}
}
current = current.getParent();
}
return null;
}
private static org.eclipse.jdt.core.dom.SwitchCase findSwitchCaseForStatement(
java.util.List<?> statements, org.eclipse.jdt.core.dom.ASTNode bodyStatement) {
int idx = statements.indexOf(bodyStatement);
if (idx < 0) {
return null;
}
for (int i = idx - 1; i >= 0; i--) {
if (statements.get(i) instanceof org.eclipse.jdt.core.dom.SwitchCase switchCase) {
return switchCase;
}
}
return null;
}
private static String buildSwitchCaseConstraint(org.eclipse.jdt.core.dom.SwitchCase switchCase) {
if (switchCase == null || switchCase.isDefault()) {
return null;
}
org.eclipse.jdt.core.dom.Expression switchSelector = null;
org.eclipse.jdt.core.dom.ASTNode switchParent = switchCase.getParent();
if (switchParent instanceof org.eclipse.jdt.core.dom.SwitchStatement ss) {
switchSelector = ss.getExpression();
} else if (switchParent instanceof org.eclipse.jdt.core.dom.SwitchExpression se) {
switchSelector = se.getExpression();
}
if (switchSelector == null) {
return null;
}
java.util.List<String> caseLabels = new java.util.ArrayList<>();
for (Object exprObj : switchCase.expressions()) {
if (exprObj instanceof org.eclipse.jdt.core.dom.Expression caseExpr) {
caseLabels.add(caseExpr.toString());
}
}
if (caseLabels.isEmpty()) {
return null;
}
String selector = switchSelector.toString();
if (caseLabels.size() == 1) {
return selector + " == " + caseLabels.get(0);
}
StringBuilder combined = new StringBuilder("(");
for (int i = 0; i < caseLabels.size(); i++) {
if (i > 0) {
combined.append(" || ");
}
combined.append(selector).append(" == ").append(caseLabels.get(i));
}
combined.append(')');
return combined.toString();
}
} }

View File

@@ -323,6 +323,9 @@ public class JdtDataFlowModel implements DataFlowModel {
Map<IVariableBinding, Expression> paramBindings, Map<IVariableBinding, Expression> paramBindings,
Map<IVariableBinding, Expression> instanceFieldBindings, Map<IVariableBinding, Expression> instanceFieldBindings,
int depth) { int depth) {
if (!context.isInlineAccessors()) {
return List.of();
}
if (ResolutionBudget.defaults().isAccessorInlineExhausted(depth)) { if (ResolutionBudget.defaults().isAccessorInlineExhausted(depth)) {
return List.of(); return List.of();
} }
@@ -361,9 +364,17 @@ public class JdtDataFlowModel implements DataFlowModel {
mi.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1); mi.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1);
} }
if (accessor.isPresent() if (accessor.isPresent()) {
&& shouldSkipAccessorDueToConcreteOverride(declaringFqn, methodName, receiverDefs)) { String accessorOwner = accessor.get().ownerFqn();
return List.of(); if (accessorOwner.equals(declaringFqn)) {
Optional<AccessorSummary> concreteAccessor =
lookupConcreteAccessorOverride(declaringFqn, methodName, receiverDefs);
if (concreteAccessor.isPresent()) {
accessor = concreteAccessor;
} else if (shouldSkipAccessorDueToConcreteOverride(declaringFqn, methodName, receiverDefs)) {
return List.of();
}
}
} }
if (accessor.isEmpty()) { if (accessor.isEmpty()) {
@@ -412,6 +423,23 @@ public class JdtDataFlowModel implements DataFlowModel {
return results; return results;
} }
private Optional<AccessorSummary> lookupConcreteAccessorOverride(
String declaringFqn,
String methodName,
List<Expression> receiverDefs) {
for (Expression receiverDef : receiverDefs) {
String concreteFqn = concreteReceiverTypeFqn(receiverDef);
if (concreteFqn == null || concreteFqn.equals(declaringFqn)) {
continue;
}
Optional<AccessorSummary> concreteAccessor = context.getAccessorIndex().lookup(concreteFqn, methodName);
if (concreteAccessor.isPresent() && concreteAccessor.get().isGetter()) {
return concreteAccessor;
}
}
return Optional.empty();
}
private boolean shouldSkipAccessorDueToConcreteOverride( private boolean shouldSkipAccessorDueToConcreteOverride(
String declaringFqn, String declaringFqn,
String methodName, String methodName,

View File

@@ -550,6 +550,28 @@ class AccessorInliningDataFlowTest {
assertResolvedValue(tempDir, "value", "SUPER"); assertResolvedValue(tempDir, "value", "SUPER");
} }
@Test
void shouldResolveInterfaceEnumGetterViaConcreteImplementation(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public interface RichEvent {
EventType getType();
}
public static class PayEvent implements RichEvent {
public EventType getType() { return EventType.PAY; }
}
public void run() {
RichEvent event = new PayEvent();
EventType value = event.getType();
}
}
enum EventType { PAY, SHIP }
""");
assertResolvedValue(tempDir, "value", "EventType.PAY");
}
@Test @Test
void shouldReturnEmptyWhenAccessorIndexHasNoEntry(@TempDir Path tempDir) throws IOException { void shouldReturnEmptyWhenAccessorIndexHasNoEntry(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """ writeJava(tempDir, "com/example/Runner.java", """

View File

@@ -0,0 +1,176 @@
package click.kamil.springstatemachineexporter.analysis.pipeline;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class AccessorPipelineBugFixTest {
@TempDir
Path tempDir;
CodebaseContext context;
ConstantExtractor constantExtractor;
ConstructorAnalyzer constructorAnalyzer;
AccessorResolver accessorResolver;
VariableTracer variableTracer;
@BeforeEach
void setUp() throws IOException {
context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
ConstantResolver constantResolver = context.getConstantResolver();
constantExtractor = new ConstantExtractor(context, constantResolver, mi -> null);
constructorAnalyzer = new ConstructorAnalyzer(context, constantResolver);
variableTracer = new VariableTracer(context, constantResolver);
constantExtractor.setVariableTracer(variableTracer);
constantExtractor.setConstructorAnalyzer(constructorAnalyzer);
accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
constantExtractor.setAccessorResolver(accessorResolver);
}
private void writeJava(String relativePath, String source) throws IOException {
Path file = tempDir.resolve(relativePath);
Files.createDirectories(file.getParent());
Files.writeString(file, source);
}
private void scanWorkspace() throws IOException {
context.scan(tempDir);
}
@Nested
class ConstructorAnalyzerSubclassPollution {
@Test
void shouldNotPullSubclassFieldValuesWhenTracingConcreteBaseType() throws IOException {
writeJava("com/example/Base.java", """
package com.example;
public class Base {
protected String token;
}
""");
writeJava("com/example/Sub.java", """
package com.example;
public class Sub extends Base {
public Sub() { this.token = "SUB_ONLY"; }
}
""");
scanWorkspace();
TypeDeclaration baseTd = context.getTypeDeclaration("com.example.Base");
List<String> traced = constructorAnalyzer.traceFieldInConstructors(
baseTd, "token", context, new HashSet<>(), ResolutionBudget.defaults());
assertThat(traced).doesNotContain("SUB_ONLY");
}
}
@Nested
class ConstantExtractorAbstractBodyWidening {
@Test
void shouldWidenToImplementationsWhenInterfaceMethodHasNoBody() throws IOException {
writeJava("com/example/Code.java", """
package com.example;
public enum Code { A, B }
""");
writeJava("com/example/RichEvent.java", """
package com.example;
public interface RichEvent {
Code getCode();
}
""");
writeJava("com/example/EventA.java", """
package com.example;
public class EventA implements RichEvent {
public Code getCode() { return Code.A; }
}
""");
writeJava("com/example/EventB.java", """
package com.example;
public class EventB implements RichEvent {
public Code getCode() { return Code.B; }
}
""");
scanWorkspace();
List<String> resolved = constantExtractor.resolveMethodReturnConstant(
"com.example.RichEvent", "getCode", 0, new HashSet<>(), null);
assertThat(resolved).containsExactlyInAnyOrder("Code.A", "Code.B");
}
}
@Nested
class AccessorResolverCacheKey {
@Test
void shouldNotReuseCachedResultWhenFieldValuesDiffer() throws IOException {
writeJava("com/example/Payload.java", """
package com.example;
public class Payload {
private String event;
public String getEvent() { return event; }
}
""");
scanWorkspace();
TypeDeclaration td = context.getTypeDeclaration("com.example.Payload");
Map<String, String> valuesA = new HashMap<>(Map.of("event", "PAY"));
Map<String, String> valuesB = new HashMap<>(Map.of("event", "CANCEL"));
List<String> pay = accessorResolver.resolve(
"com.example.Payload",
"getEvent",
new AccessorResolver.ReceiverContext(td, null, null, false, valuesA),
ResolutionBudget.defaults());
List<String> cancel = accessorResolver.resolve(
"com.example.Payload",
"getEvent",
new AccessorResolver.ReceiverContext(td, null, null, false, valuesB),
ResolutionBudget.defaults());
assertThat(pay).containsExactly("PAY");
assertThat(cancel).containsExactly("CANCEL");
}
}
@Nested
class InlineAccessorsDisabled {
@Test
void shouldSkipIndexedDataflowShortcutWhenDisabled() throws IOException {
writeJava("com/example/Payload.java", """
package com.example;
public class Payload {
private String event = "INLINE";
public String getEvent() { return event; }
}
""");
context.setInlineAccessors(false);
scanWorkspace();
assertThat(context.getAccessorIndex().trivialCount()).isZero();
assertThat(context.isInlineAccessors()).isFalse();
}
}
}

View File

@@ -0,0 +1,31 @@
package click.kamil.springstatemachineexporter.analysis.resolver;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class BooleanConstraintEvaluatorBindingsTest {
@Test
void shouldAcceptMatchingEnumSwitchConstraint() {
assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings(
"command == ORDER_PAY",
Map.of("command", "DomainCommand.ORDER_PAY"))).isTrue();
}
@Test
void shouldRejectMismatchedEnumSwitchConstraint() {
assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings(
"command == ORDER_SHIP",
Map.of("command", "DomainCommand.ORDER_PAY"))).isFalse();
}
@Test
void shouldAcceptMatchingStringSwitchConstraint() {
assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings(
"commandKey == \"order.pay\"",
Map.of("commandKey", "order.pay"))).isTrue();
}
}

View File

@@ -74,6 +74,32 @@ class AnalysisCacheCorrectnessTest {
assertThat(context.getCache().get("heuristicCallGraph")).isSameAs(cachedGraph); assertThat(context.getCache().get("heuristicCallGraph")).isSameAs(cachedGraph);
} }
@Test
void jdtIntelligenceProviderShouldCacheCallChains(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
class A { void go() { new B().run("PAY"); } }
class B { void run(String s) { new C().fire(com.example.E.PAY); } }
enum E { PAY, SHIP }
class C { void fire(E e) {} }
""";
Files.writeString(tempDir.resolve("App.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
JdtIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, tempDir);
EntryPoint entry = EntryPoint.builder().className("com.example.A").methodName("go").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.C").methodName("fire").event("e").build();
List<EntryPoint> entryPoints = List.of(entry);
List<TriggerPoint> triggers = List.of(trigger);
List<CallChain> first = intelligence.findCallChains(entryPoints, triggers);
List<CallChain> second = intelligence.findCallChains(entryPoints, triggers);
assertThat(second).isSameAs(first);
}
/** /**
* Two consecutive {@link CallGraphEngine#findChains} calls on a cached graph must agree. * Two consecutive {@link CallGraphEngine#findChains} calls on a cached graph must agree.
*/ */

View File

@@ -58,6 +58,50 @@ class LayeredDispatcherSampleTest {
"OrderTransitionEvent.PAY", "OrderState.NEW", "OrderState.PAID"); "OrderTransitionEvent.PAY", "OrderState.NEW", "OrderState.PAID");
assertEndpointLinksSingleTransition(orderMachine, "POST /api/orders/rich/ship", assertEndpointLinksSingleTransition(orderMachine, "POST /api/orders/rich/ship",
"OrderTransitionEvent.SHIP", "OrderState.PAID", "OrderState.SHIPPED"); "OrderTransitionEvent.SHIP", "OrderState.PAID", "OrderState.SHIPPED");
assertEndpointLinksSingleTransition(orderMachine, "POST /api/string-dispatch/orders/pay",
"OrderTransitionEvent.PAY", "OrderState.NEW", "OrderState.PAID");
assertEndpointLinksSingleTransition(orderMachine, "POST /api/string-dispatch/orders/ship",
"OrderTransitionEvent.SHIP", "OrderState.PAID", "OrderState.SHIPPED");
}
@Test
void shouldLinkOnlyOneMatchedTransitionPerStringDispatchEndpoint(@TempDir Path tempDir) throws Exception {
Path sampleRoot = findProjectRoot().resolve("state_machines/layered_dispatcher_sample");
ExportService exportService = new ExportService(List.of(new JsonExporter()));
exportService.runExporter(sampleRoot, tempDir, List.of("json"), true, List.of(), null, null,
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
JsonNode orderMachine = readMachineJson(tempDir, "StandardOrderStateMachineConfiguration", new ObjectMapper());
assertSingleMatchedChain(orderMachine, "POST /api/string-dispatch/orders/pay", "OrderTransitionEvent.PAY");
assertSingleMatchedChain(orderMachine, "POST /api/string-dispatch/orders/ship", "OrderTransitionEvent.SHIP");
}
private static void assertSingleMatchedChain(JsonNode machineJson, String endpointName, String expectedEvent) {
List<JsonNode> matchedChains = findMatchedCallChains(machineJson, endpointName);
assertThat(matchedChains).as("matched chains for %s", endpointName).hasSize(1);
assertThat(matchedChains.get(0).get("matchedTransitions")).hasSize(1);
assertThat(matchedChains.get(0).get("matchedTransitions").get(0).get("event").asText()).isEqualTo(expectedEvent);
}
private static List<JsonNode> findMatchedCallChains(JsonNode machineJson, String endpointName) {
JsonNode callChains = machineJson.path("metadata").path("callChains");
if (!callChains.isArray()) {
return List.of();
}
List<JsonNode> result = new java.util.ArrayList<>();
for (JsonNode chain : callChains) {
if (!endpointName.equals(chain.path("entryPoint").path("name").asText())) {
continue;
}
JsonNode matched = chain.get("matchedTransitions");
if (matched != null && matched.isArray() && !matched.isEmpty()) {
result.add(chain);
}
}
return result;
} }
private static JsonNode readMachineJson(Path outputDir, String configBaseName, ObjectMapper mapper) private static JsonNode readMachineJson(Path outputDir, String configBaseName, ObjectMapper mapper)

View File

@@ -0,0 +1,101 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class PathBindingEvaluatorTest {
@Test
void shouldPruneImpossibleSwitchDispatchArms(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
public class ApiController {
private final CommandGateway gateway = new CommandGateway();
public void pay() { gateway.executeViaMapper("order.pay"); }
}
enum DomainCommand { ORDER_PAY, ORDER_SHIP }
enum OrderEvent { PAY, SHIP }
class CommandGateway {
private final StringCommandMapper mapper = new StringCommandMapper();
private final CentralDispatcher central = new CentralDispatcher();
void executeViaMapper(String commandKey) {
DomainCommand command = mapper.fromString(commandKey);
central.dispatch(command);
}
}
class StringCommandMapper {
DomainCommand fromString(String commandKey) {
return switch (commandKey) {
case "order.pay" -> DomainCommand.ORDER_PAY;
case "order.ship" -> DomainCommand.ORDER_SHIP;
default -> throw new IllegalArgumentException(commandKey);
};
}
}
class CentralDispatcher {
void dispatch(DomainCommand command) {
switch (command) {
case ORDER_PAY -> orderPay();
case ORDER_SHIP -> orderShip();
}
}
void orderPay() { fire(OrderEvent.PAY); }
void orderShip() { fire(OrderEvent.SHIP); }
void fire(OrderEvent event) {}
}
""";
Files.writeString(tempDir.resolve("App.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
Map<String, List<CallEdge>> graph = engine.buildCallGraph();
CallGraphPathFinder pathFinder = new CallGraphPathFinder(context);
List<List<String>> rawPaths = pathFinder.findAllPaths(
"com.example.ApiController.pay",
"com.example.CentralDispatcher.fire",
graph,
new java.util.HashSet<>());
assertThat(rawPaths).hasSize(2);
PathBindingEvaluator evaluator = new PathBindingEvaluator(
context, engine.variableTracer, context.getConstantResolver(), engine.typeResolver);
List<CallEdge> dispatchEdges = graph.get("com.example.CentralDispatcher.dispatch");
assertThat(dispatchEdges.stream().map(CallEdge::getConstraint).filter(c -> c != null && !c.isBlank()))
.isNotEmpty();
boolean payPathCompatible = evaluator.isPathCompatible(rawPaths.get(0), graph, pathFinder);
Map<String, String> payBindings = evaluator.traceBindings(rawPaths.get(0), graph, pathFinder);
assertThat(payBindings)
.as("pay path bindings, compatible=%s", payPathCompatible)
.containsEntry("command", "DomainCommand.ORDER_PAY");
EntryPoint entry = EntryPoint.builder().className("com.example.ApiController").methodName("pay").build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.CentralDispatcher")
.methodName("fire")
.event("event")
.build();
List<click.kamil.springstatemachineexporter.analysis.model.CallChain> chains =
engine.findChains(List.of(entry), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getMethodChain()).contains("com.example.CentralDispatcher.orderPay");
}
}

View File

@@ -0,0 +1,79 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
import click.kamil.springstatemachineexporter.service.ExportService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.StreamSupport;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Verifies switch-case branch pruning for string-key dispatch chains.
*/
class StringDispatchCallGraphGapTest {
private static Path findProjectRoot() {
Path current = Path.of(".").toAbsolutePath();
while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
current = current.getParent();
}
return current;
}
@Test
void payStringDispatchEndpointShouldMatchOnlyPayTransition(@TempDir Path tempDir) throws Exception {
Path sampleRoot = findProjectRoot().resolve("state_machines/layered_dispatcher_sample");
ExportService exportService = new ExportService(List.of(new JsonExporter()));
exportService.runExporter(sampleRoot, tempDir, List.of("json"), true, List.of(), null, null,
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
JsonNode orderMachine = readMachineJson(tempDir, "StandardOrderStateMachineConfiguration", new ObjectMapper());
List<JsonNode> matchedChains = findMatchedCallChains(orderMachine, "POST /api/string-dispatch/orders/pay");
assertThat(matchedChains).hasSize(1);
assertThat(matchedChains.get(0).get("matchedTransitions")).hasSize(1);
assertThat(matchedChains.get(0).get("matchedTransitions").get(0).get("event").asText())
.isEqualTo("OrderTransitionEvent.PAY");
}
private static JsonNode readMachineJson(Path outputDir, String configBaseName, ObjectMapper mapper)
throws Exception {
Path machineDir;
try (var stream = Files.list(outputDir)) {
machineDir = stream
.filter(Files::isDirectory)
.filter(path -> path.getFileName().toString().endsWith(configBaseName))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No output for " + configBaseName));
}
Path jsonFile = machineDir.resolve(machineDir.getFileName() + ".json");
return mapper.readTree(jsonFile.toFile());
}
private static List<JsonNode> findMatchedCallChains(JsonNode machineJson, String endpointName) {
JsonNode callChains = machineJson.path("metadata").path("callChains");
if (!callChains.isArray()) {
return List.of();
}
List<JsonNode> result = new ArrayList<>();
for (JsonNode chain : callChains) {
if (!endpointName.equals(chain.path("entryPoint").path("name").asText())) {
continue;
}
JsonNode matched = chain.get("matchedTransitions");
if (matched != null && matched.isArray() && !matched.isEmpty()) {
result.add(chain);
}
}
return result;
}
}

View File

@@ -0,0 +1,59 @@
package click.kamil.springstatemachineexporter.ast.common;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
class AstUtilsSwitchConstraintTest {
@Test
void shouldExtractSwitchCaseConstraintForArrowSwitch(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
class Demo {
void dispatch(DomainCommand command) {
switch (command) {
case ORDER_PAY -> orderPay();
case ORDER_SHIP -> orderShip();
}
}
void orderPay() {}
void orderShip() {}
}
enum DomainCommand { ORDER_PAY, ORDER_SHIP }
""";
Files.writeString(tempDir.resolve("Demo.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
AtomicReference<MethodInvocation> orderPayCall = new AtomicReference<>();
for (CompilationUnit cu : context.getCompilationUnits()) {
cu.accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
if ("orderPay".equals(node.getName().getIdentifier())
&& node.getExpression() == null) {
orderPayCall.set(node);
}
return super.visit(node);
}
});
}
if (orderPayCall.get() == null) {
org.junit.jupiter.api.Assertions.fail("orderPay MethodInvocation not found in AST");
}
assertThat(AstUtils.findEnclosingSwitchCase(orderPayCall.get())).isNotNull();
String constraint = AstUtils.findConditionConstraint(orderPayCall.get());
assertThat(constraint).contains("command == ORDER_PAY");
}
}

View File

@@ -1,19 +1,6 @@
{ {
"metadata" : { "metadata" : {
"triggers" : [ { "triggers" : [ {
"event" : "event",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : null,
"external" : false,
"constraint" : null,
"ambiguous" : false
}, {
"event" : "event", "event" : "event",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher", "className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent", "methodName" : "sendDocumentEvent",
@@ -154,96 +141,6 @@
} ] } ]
} ], } ],
"callChains" : [ { "callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/pay",
"className" : "click.kamil.examples.statemachine.layered.web.OrderController",
"methodName" : "pay",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/OrderController.java",
"metadata" : {
"path" : "/api/orders/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.OrderController.pay", "click.kamil.examples.statemachine.layered.web.CommandGateway.payOrder", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderPay", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.PAY",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.PAY" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/ship",
"className" : "click.kamil.examples.statemachine.layered.web.OrderController",
"methodName" : "ship",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/OrderController.java",
"metadata" : {
"path" : "/api/orders/ship",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.OrderController.ship", "click.kamil.examples.statemachine.layered.web.CommandGateway.shipOrder", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderShip", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.SHIP",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.SHIP" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/cancel",
"className" : "click.kamil.examples.statemachine.layered.web.OrderController",
"methodName" : "cancel",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/OrderController.java",
"metadata" : {
"path" : "/api/orders/cancel",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.OrderController.cancel", "click.kamil.examples.statemachine.layered.web.CommandGateway.cancelOrder", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderCancel", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.CANCEL",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.CANCEL" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : { "entryPoint" : {
"type" : "REST", "type" : "REST",
"name" : "POST /api/documents/submit", "name" : "POST /api/documents/submit",
@@ -345,552 +242,6 @@
"targetState" : "DocumentState.REJECTED", "targetState" : "DocumentState.REJECTED",
"event" : "DocumentTransitionEvent.REJECT" "event" : "DocumentTransitionEvent.REJECT"
} ] } ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/pay",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "payWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.payWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderPay", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.PAY",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.PAY" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/pay",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "payWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.payWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderShip", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.SHIP",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.SHIP" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/pay",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "payWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.payWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderCancel", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.CANCEL",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.CANCEL" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/pay",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "payWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.payWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentSubmit", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.SUBMIT",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.SUBMIT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "DocumentState.DRAFT",
"targetState" : "DocumentState.SUBMITTED",
"event" : "DocumentTransitionEvent.SUBMIT"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/pay",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "payWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.payWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentApprove", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.APPROVE",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.APPROVE" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "DocumentState.SUBMITTED",
"targetState" : "DocumentState.APPROVED",
"event" : "DocumentTransitionEvent.APPROVE"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/pay",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "payWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.payWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentReject", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.REJECT",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.REJECT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "DocumentState.SUBMITTED",
"targetState" : "DocumentState.REJECTED",
"event" : "DocumentTransitionEvent.REJECT"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/ship",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "shipWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/ship",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.shipWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderPay", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.PAY",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.PAY" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/ship",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "shipWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/ship",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.shipWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderShip", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.SHIP",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.SHIP" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/ship",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "shipWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/ship",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.shipWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderCancel", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.CANCEL",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.CANCEL" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/ship",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "shipWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/ship",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.shipWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentSubmit", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.SUBMIT",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.SUBMIT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "DocumentState.DRAFT",
"targetState" : "DocumentState.SUBMITTED",
"event" : "DocumentTransitionEvent.SUBMIT"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/ship",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "shipWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/ship",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.shipWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentApprove", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.APPROVE",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.APPROVE" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "DocumentState.SUBMITTED",
"targetState" : "DocumentState.APPROVED",
"event" : "DocumentTransitionEvent.APPROVE"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/ship",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "shipWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/ship",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.shipWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentReject", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.REJECT",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.REJECT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "DocumentState.SUBMITTED",
"targetState" : "DocumentState.REJECTED",
"event" : "DocumentTransitionEvent.REJECT"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/rich/pay",
"className" : "click.kamil.examples.statemachine.layered.web.RichOrderController",
"methodName" : "payViaRichEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/RichOrderController.java",
"metadata" : {
"path" : "/api/orders/rich/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.RichOrderController.payViaRichEvent", "click.kamil.examples.statemachine.layered.web.CommandGateway.payOrderViaRichEvent", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderPayViaRichEvent", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.PAY",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.PAY" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/rich/ship",
"className" : "click.kamil.examples.statemachine.layered.web.RichOrderController",
"methodName" : "shipViaRichEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/RichOrderController.java",
"metadata" : {
"path" : "/api/orders/rich/ship",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.RichOrderController.shipViaRichEvent", "click.kamil.examples.statemachine.layered.web.CommandGateway.shipOrderViaRichEvent", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderShipViaRichEvent", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.SHIP",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.SHIP" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/commands/{commandKey}",
"className" : "click.kamil.examples.statemachine.layered.web.GenericCommandController",
"methodName" : "execute",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/GenericCommandController.java",
"metadata" : {
"path" : "/api/commands/{commandKey}",
"verb" : "POST"
},
"parameters" : [ {
"name" : "commandKey",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.GenericCommandController.execute", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderPay", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.PAY",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.PAY" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/commands/{commandKey}",
"className" : "click.kamil.examples.statemachine.layered.web.GenericCommandController",
"methodName" : "execute",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/GenericCommandController.java",
"metadata" : {
"path" : "/api/commands/{commandKey}",
"verb" : "POST"
},
"parameters" : [ {
"name" : "commandKey",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.GenericCommandController.execute", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderShip", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.SHIP",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.SHIP" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/commands/{commandKey}",
"className" : "click.kamil.examples.statemachine.layered.web.GenericCommandController",
"methodName" : "execute",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/GenericCommandController.java",
"metadata" : {
"path" : "/api/commands/{commandKey}",
"verb" : "POST"
},
"parameters" : [ {
"name" : "commandKey",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.GenericCommandController.execute", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderCancel", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.CANCEL",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.CANCEL" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, { }, {
"entryPoint" : { "entryPoint" : {
"type" : "REST", "type" : "REST",
@@ -920,7 +271,7 @@
"lineNumber" : 81, "lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.SUBMIT" ], "polymorphicEvents" : [ "DocumentTransitionEvent.SUBMIT" ],
"external" : false, "external" : false,
"constraint" : null, "constraint" : "command == DOCUMENT_SUBMIT",
"ambiguous" : false "ambiguous" : false
}, },
"contextMachineId" : null, "contextMachineId" : null,
@@ -958,7 +309,7 @@
"lineNumber" : 81, "lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.APPROVE" ], "polymorphicEvents" : [ "DocumentTransitionEvent.APPROVE" ],
"external" : false, "external" : false,
"constraint" : null, "constraint" : "command == DOCUMENT_APPROVE",
"ambiguous" : false "ambiguous" : false
}, },
"contextMachineId" : null, "contextMachineId" : null,
@@ -996,7 +347,7 @@
"lineNumber" : 81, "lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.REJECT" ], "polymorphicEvents" : [ "DocumentTransitionEvent.REJECT" ],
"external" : false, "external" : false,
"constraint" : null, "constraint" : "command == DOCUMENT_REJECT",
"ambiguous" : false "ambiguous" : false
}, },
"contextMachineId" : null, "contextMachineId" : null,

View File

@@ -13,19 +13,6 @@
"external" : false, "external" : false,
"constraint" : null, "constraint" : null,
"ambiguous" : false "ambiguous" : false
}, {
"event" : "event",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : null,
"external" : false,
"constraint" : null,
"ambiguous" : false
} ], } ],
"entryPoints" : [ { "entryPoints" : [ {
"type" : "REST", "type" : "REST",
@@ -255,96 +242,6 @@
"targetState" : "OrderState.CANCELLED", "targetState" : "OrderState.CANCELLED",
"event" : "OrderTransitionEvent.CANCEL" "event" : "OrderTransitionEvent.CANCEL"
} ] } ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/documents/submit",
"className" : "click.kamil.examples.statemachine.layered.web.DocumentController",
"methodName" : "submit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/DocumentController.java",
"metadata" : {
"path" : "/api/documents/submit",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.DocumentController.submit", "click.kamil.examples.statemachine.layered.web.CommandGateway.submitDocument", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentSubmit", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.SUBMIT",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.SUBMIT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/documents/approve",
"className" : "click.kamil.examples.statemachine.layered.web.DocumentController",
"methodName" : "approve",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/DocumentController.java",
"metadata" : {
"path" : "/api/documents/approve",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.DocumentController.approve", "click.kamil.examples.statemachine.layered.web.CommandGateway.approveDocument", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentApprove", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.APPROVE",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.APPROVE" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/documents/reject",
"className" : "click.kamil.examples.statemachine.layered.web.DocumentController",
"methodName" : "reject",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/DocumentController.java",
"metadata" : {
"path" : "/api/documents/reject",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.DocumentController.reject", "click.kamil.examples.statemachine.layered.web.CommandGateway.rejectDocument", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentReject", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.REJECT",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.REJECT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, { }, {
"entryPoint" : { "entryPoint" : {
"type" : "REST", "type" : "REST",
@@ -370,199 +267,7 @@
"lineNumber" : 75, "lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.PAY" ], "polymorphicEvents" : [ "OrderTransitionEvent.PAY" ],
"external" : false, "external" : false,
"constraint" : null, "constraint" : "command == ORDER_PAY",
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderState.NEW",
"targetState" : "OrderState.PAID",
"event" : "OrderTransitionEvent.PAY"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/pay",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "payWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.payWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderShip", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.SHIP",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.SHIP" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderState.PAID",
"targetState" : "OrderState.SHIPPED",
"event" : "OrderTransitionEvent.SHIP"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/pay",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "payWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.payWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderCancel", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.CANCEL",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.CANCEL" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderState.PAID",
"targetState" : "OrderState.CANCELLED",
"event" : "OrderTransitionEvent.CANCEL"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/pay",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "payWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.payWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentSubmit", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.SUBMIT",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.SUBMIT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/pay",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "payWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.payWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentApprove", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.APPROVE",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.APPROVE" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/pay",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "payWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.payWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentReject", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.REJECT",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.REJECT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/ship",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "shipWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/ship",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.shipWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderPay", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.PAY",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.PAY" ],
"external" : false,
"constraint" : null,
"ambiguous" : false "ambiguous" : false
}, },
"contextMachineId" : null, "contextMachineId" : null,
@@ -596,7 +301,7 @@
"lineNumber" : 75, "lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.SHIP" ], "polymorphicEvents" : [ "OrderTransitionEvent.SHIP" ],
"external" : false, "external" : false,
"constraint" : null, "constraint" : "command == ORDER_SHIP",
"ambiguous" : false "ambiguous" : false
}, },
"contextMachineId" : null, "contextMachineId" : null,
@@ -605,130 +310,6 @@
"targetState" : "OrderState.SHIPPED", "targetState" : "OrderState.SHIPPED",
"event" : "OrderTransitionEvent.SHIP" "event" : "OrderTransitionEvent.SHIP"
} ] } ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/ship",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "shipWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/ship",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.shipWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.orderCancel", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendOrderEvent" ],
"triggerPoint" : {
"event" : "OrderTransitionEvent.CANCEL",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendOrderEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.CANCEL" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderState.PAID",
"targetState" : "OrderState.CANCELLED",
"event" : "OrderTransitionEvent.CANCEL"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/ship",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "shipWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/ship",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.shipWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentSubmit", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.SUBMIT",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.SUBMIT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/ship",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "shipWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/ship",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.shipWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentApprove", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.APPROVE",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.APPROVE" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/string-dispatch/orders/ship",
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
"methodName" : "shipWithStringKey",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
"metadata" : {
"path" : "/api/string-dispatch/orders/ship",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.StringDispatchController.shipWithStringKey", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentReject", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.REJECT",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.REJECT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, { }, {
"entryPoint" : { "entryPoint" : {
"type" : "REST", "type" : "REST",
@@ -826,7 +407,7 @@
"lineNumber" : 75, "lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.PAY" ], "polymorphicEvents" : [ "OrderTransitionEvent.PAY" ],
"external" : false, "external" : false,
"constraint" : null, "constraint" : "command == ORDER_PAY",
"ambiguous" : false "ambiguous" : false
}, },
"contextMachineId" : null, "contextMachineId" : null,
@@ -864,7 +445,7 @@
"lineNumber" : 75, "lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.SHIP" ], "polymorphicEvents" : [ "OrderTransitionEvent.SHIP" ],
"external" : false, "external" : false,
"constraint" : null, "constraint" : "command == ORDER_SHIP",
"ambiguous" : false "ambiguous" : false
}, },
"contextMachineId" : null, "contextMachineId" : null,
@@ -902,7 +483,7 @@
"lineNumber" : 75, "lineNumber" : 75,
"polymorphicEvents" : [ "OrderTransitionEvent.CANCEL" ], "polymorphicEvents" : [ "OrderTransitionEvent.CANCEL" ],
"external" : false, "external" : false,
"constraint" : null, "constraint" : "command == ORDER_CANCEL",
"ambiguous" : false "ambiguous" : false
}, },
"contextMachineId" : null, "contextMachineId" : null,
@@ -911,108 +492,6 @@
"targetState" : "OrderState.CANCELLED", "targetState" : "OrderState.CANCELLED",
"event" : "OrderTransitionEvent.CANCEL" "event" : "OrderTransitionEvent.CANCEL"
} ] } ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/commands/{commandKey}",
"className" : "click.kamil.examples.statemachine.layered.web.GenericCommandController",
"methodName" : "execute",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/GenericCommandController.java",
"metadata" : {
"path" : "/api/commands/{commandKey}",
"verb" : "POST"
},
"parameters" : [ {
"name" : "commandKey",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.GenericCommandController.execute", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentSubmit", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.SUBMIT",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.SUBMIT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/commands/{commandKey}",
"className" : "click.kamil.examples.statemachine.layered.web.GenericCommandController",
"methodName" : "execute",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/GenericCommandController.java",
"metadata" : {
"path" : "/api/commands/{commandKey}",
"verb" : "POST"
},
"parameters" : [ {
"name" : "commandKey",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.GenericCommandController.execute", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentApprove", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.APPROVE",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.APPROVE" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/commands/{commandKey}",
"className" : "click.kamil.examples.statemachine.layered.web.GenericCommandController",
"methodName" : "execute",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/GenericCommandController.java",
"metadata" : {
"path" : "/api/commands/{commandKey}",
"verb" : "POST"
},
"parameters" : [ {
"name" : "commandKey",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.layered.web.GenericCommandController.execute", "click.kamil.examples.statemachine.layered.web.CommandGateway.executeViaMapper", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.dispatch", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.documentReject", "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher.sendDocumentEvent" ],
"triggerPoint" : {
"event" : "DocumentTransitionEvent.REJECT",
"className" : "click.kamil.examples.statemachine.layered.dispatch.CentralEventDispatcher",
"methodName" : "sendDocumentEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/dispatch/CentralEventDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "DocumentTransitionEvent.REJECT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
} ], } ],
"properties" : { "properties" : {
"default" : { } "default" : { }