Split boolean ternary endpoints into provable branches and scope payment exports.
Boolean request-param ternaries now expand into true/false binding variants so each call chain resolves a single event instead of AMBIGUOUS_WIDEN. Setter/constructor @Qualifier injection is traced for stateMachineId, and PaymentStateMachineConfig gets a dedicated golden with payment endpoints linked via paymentStateMachine. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -102,6 +102,9 @@ public final class BooleanConstraintEvaluator {
|
|||||||
if (boundValue.startsWith("\"") && boundValue.endsWith("\"")) {
|
if (boundValue.startsWith("\"") && boundValue.endsWith("\"")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if ("true".equalsIgnoreCase(boundValue) || "false".equalsIgnoreCase(boundValue)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (boundValue.contains(".") && Character.isUpperCase(boundValue.charAt(boundValue.lastIndexOf('.') + 1))) {
|
if (boundValue.contains(".") && Character.isUpperCase(boundValue.charAt(boundValue.lastIndexOf('.') + 1))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -174,6 +177,10 @@ public final class BooleanConstraintEvaluator {
|
|||||||
matcher.appendReplacement(sb, matches ? "true" : "false");
|
matcher.appendReplacement(sb, matches ? "true" : "false");
|
||||||
}
|
}
|
||||||
matcher.appendTail(sb);
|
matcher.appendTail(sb);
|
||||||
|
if ("true".equalsIgnoreCase(cleanValue) || "false".equalsIgnoreCase(cleanValue)) {
|
||||||
|
Pattern bareVar = Pattern.compile("(?<![\\w.])" + Pattern.quote(varName) + "(?![\\w])");
|
||||||
|
return bareVar.matcher(expr).replaceAll(cleanValue.toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
startMethods.add(startMethod);
|
startMethods.add(startMethod);
|
||||||
|
|
||||||
List<Map<String, String>> bindingVariants =
|
List<Map<String, String>> bindingVariants =
|
||||||
EntryPointBindingExpander.expandPathVariableBindings(ep, context, callGraph);
|
EntryPointBindingExpander.expandEntryPointBindings(ep, context, callGraph);
|
||||||
if (bindingVariants.isEmpty()) {
|
if (bindingVariants.isEmpty()) {
|
||||||
bindingVariants = List.of(Map.of());
|
bindingVariants = List.of(Map.of());
|
||||||
}
|
}
|
||||||
@@ -213,6 +213,36 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return triggerConstraint;
|
return triggerConstraint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Expression selectTernaryBranch(ConditionalExpression cond, Map<String, String> bindings) {
|
||||||
|
if (cond == null || bindings == null || bindings.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Expression condition = cond.getExpression();
|
||||||
|
if (condition instanceof PrefixExpression prefixExpression
|
||||||
|
&& prefixExpression.getOperator() == PrefixExpression.Operator.NOT
|
||||||
|
&& prefixExpression.getOperand() instanceof SimpleName negatedName) {
|
||||||
|
String bound = bindings.get(negatedName.getIdentifier());
|
||||||
|
if ("true".equals(bound)) {
|
||||||
|
return cond.getElseExpression();
|
||||||
|
}
|
||||||
|
if ("false".equals(bound)) {
|
||||||
|
return cond.getThenExpression();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!(condition instanceof SimpleName simpleName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String bound = bindings.get(simpleName.getIdentifier());
|
||||||
|
if ("true".equals(bound)) {
|
||||||
|
return cond.getThenExpression();
|
||||||
|
}
|
||||||
|
if ("false".equals(bound)) {
|
||||||
|
return cond.getElseExpression();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected TriggerPoint resolveTriggerPointParametersOriginal(
|
protected TriggerPoint resolveTriggerPointParametersOriginal(
|
||||||
TriggerPoint tp,
|
TriggerPoint tp,
|
||||||
List<String> path,
|
List<String> path,
|
||||||
@@ -679,9 +709,16 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
polymorphicEvents.add(declaredType);
|
polymorphicEvents.add(declaredType);
|
||||||
}
|
}
|
||||||
} else if (exprNode instanceof ConditionalExpression cond) {
|
} else if (exprNode instanceof ConditionalExpression cond) {
|
||||||
|
Expression selectedBranch = selectTernaryBranch(cond, initialBindings);
|
||||||
List<Expression> branches = new ArrayList<>();
|
List<Expression> branches = new ArrayList<>();
|
||||||
branches.add(cond.getThenExpression());
|
if (selectedBranch != null) {
|
||||||
branches.add(cond.getElseExpression());
|
branches.add(selectedBranch);
|
||||||
|
resolvedValue = selectedBranch.toString();
|
||||||
|
exprNode = selectedBranch;
|
||||||
|
} else {
|
||||||
|
branches.add(cond.getThenExpression());
|
||||||
|
branches.add(cond.getElseExpression());
|
||||||
|
}
|
||||||
for (Expression branch : branches) {
|
for (Expression branch : branches) {
|
||||||
if (branch instanceof ConditionalExpression nestedCond) {
|
if (branch instanceof ConditionalExpression nestedCond) {
|
||||||
// traceVariableAll collapses literal "true"/"false" conditions to a single branch;
|
// traceVariableAll collapses literal "true"/"false" conditions to a single branch;
|
||||||
@@ -2192,6 +2229,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
if (value == null || value.isBlank() || value.contains("(")) {
|
if (value == null || value.isBlank() || value.contains("(")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if ("true".equals(value) || "false".equals(value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
parts.add("\"" + value + "\".equalsIgnoreCase(" + entry.getKey() + ")");
|
parts.add("\"" + value + "\".equalsIgnoreCase(" + entry.getKey() + ")");
|
||||||
}
|
}
|
||||||
return parts.isEmpty() ? null : String.join(" && ", parts);
|
return parts.isEmpty() ? null : String.join(" && ", parts);
|
||||||
|
|||||||
@@ -16,6 +16,33 @@ public final class EntryPointBindingExpander {
|
|||||||
private EntryPointBindingExpander() {
|
private EntryPointBindingExpander() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands an entry point into concrete binding variants from path/query parameters:
|
||||||
|
* string switch/if arms and boolean ternary conditions provable in source.
|
||||||
|
*/
|
||||||
|
public static List<Map<String, String>> expandEntryPointBindings(
|
||||||
|
EntryPoint entryPoint,
|
||||||
|
CodebaseContext context,
|
||||||
|
Map<String, List<CallEdge>> callGraph) {
|
||||||
|
List<Map<String, String>> pathVariants = expandPathVariableBindings(entryPoint, context, callGraph);
|
||||||
|
List<Map<String, String>> booleanVariants = expandBooleanTernaryBindings(entryPoint, context, callGraph);
|
||||||
|
if (pathVariants.isEmpty()) {
|
||||||
|
return booleanVariants;
|
||||||
|
}
|
||||||
|
if (booleanVariants.isEmpty()) {
|
||||||
|
return pathVariants;
|
||||||
|
}
|
||||||
|
List<Map<String, String>> combined = new ArrayList<>();
|
||||||
|
for (Map<String, String> pathVariant : pathVariants) {
|
||||||
|
for (Map<String, String> booleanVariant : booleanVariants) {
|
||||||
|
Map<String, String> merged = new LinkedHashMap<>(pathVariant);
|
||||||
|
merged.putAll(booleanVariant);
|
||||||
|
combined.add(merged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return combined;
|
||||||
|
}
|
||||||
|
|
||||||
public static List<Map<String, String>> expandPathVariableBindings(
|
public static List<Map<String, String>> expandPathVariableBindings(
|
||||||
EntryPoint entryPoint,
|
EntryPoint entryPoint,
|
||||||
CodebaseContext context,
|
CodebaseContext context,
|
||||||
@@ -55,14 +82,37 @@ public final class EntryPointBindingExpander {
|
|||||||
Map<String, String> metadata = entryPoint.getMetadata() == null
|
Map<String, String> metadata = entryPoint.getMetadata() == null
|
||||||
? new LinkedHashMap<>()
|
? new LinkedHashMap<>()
|
||||||
: new LinkedHashMap<>(entryPoint.getMetadata());
|
: new LinkedHashMap<>(entryPoint.getMetadata());
|
||||||
|
StringBuilder querySuffix = new StringBuilder();
|
||||||
for (Map.Entry<String, String> binding : bindings.entrySet()) {
|
for (Map.Entry<String, String> binding : bindings.entrySet()) {
|
||||||
String placeholder = "{" + binding.getKey() + "}";
|
String placeholder = "{" + binding.getKey() + "}";
|
||||||
|
boolean pathPlaceholder = name != null && name.contains(placeholder);
|
||||||
|
String path = metadata.get("path");
|
||||||
|
if (!pathPlaceholder && path != null) {
|
||||||
|
pathPlaceholder = path.contains(placeholder);
|
||||||
|
}
|
||||||
|
if (pathPlaceholder) {
|
||||||
|
if (name != null) {
|
||||||
|
name = name.replace(placeholder, binding.getValue());
|
||||||
|
}
|
||||||
|
if (path != null) {
|
||||||
|
metadata.put("path", path.replace(placeholder, binding.getValue()));
|
||||||
|
}
|
||||||
|
} else if (isBooleanBindingValue(binding.getValue())) {
|
||||||
|
if (querySuffix.isEmpty()) {
|
||||||
|
querySuffix.append('?');
|
||||||
|
} else {
|
||||||
|
querySuffix.append('&');
|
||||||
|
}
|
||||||
|
querySuffix.append(binding.getKey()).append('=').append(binding.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!querySuffix.isEmpty()) {
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
name = name.replace(placeholder, binding.getValue());
|
name = name + querySuffix;
|
||||||
}
|
}
|
||||||
String path = metadata.get("path");
|
String path = metadata.get("path");
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
metadata.put("path", path.replace(placeholder, binding.getValue()));
|
metadata.put("path", path + querySuffix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return EntryPoint.builder()
|
return EntryPoint.builder()
|
||||||
@@ -92,6 +142,116 @@ public final class EntryPointBindingExpander {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<Map<String, String>> expandBooleanTernaryBindings(
|
||||||
|
EntryPoint entryPoint,
|
||||||
|
CodebaseContext context,
|
||||||
|
Map<String, List<CallEdge>> callGraph) {
|
||||||
|
if (entryPoint == null || entryPoint.getParameters() == null || entryPoint.getParameters().isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
|
||||||
|
if (!isBooleanType(parameter.getType())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (hasBooleanTernaryForParameter(
|
||||||
|
entryPoint.getClassName() + "." + entryPoint.getMethodName(),
|
||||||
|
parameter.getName(),
|
||||||
|
context,
|
||||||
|
callGraph)) {
|
||||||
|
return List.of(
|
||||||
|
Map.of(parameter.getName(), "true"),
|
||||||
|
Map.of(parameter.getName(), "false"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBooleanType(String type) {
|
||||||
|
return "boolean".equals(type) || "Boolean".equals(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBooleanBindingValue(String value) {
|
||||||
|
return "true".equals(value) || "false".equals(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasBooleanTernaryForParameter(
|
||||||
|
String startMethod,
|
||||||
|
String paramName,
|
||||||
|
CodebaseContext context,
|
||||||
|
Map<String, List<CallEdge>> callGraph) {
|
||||||
|
ArrayDeque<String> queue = new ArrayDeque<>();
|
||||||
|
Set<String> visited = new HashSet<>();
|
||||||
|
queue.add(startMethod);
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
String methodFqn = queue.poll();
|
||||||
|
if (!visited.add(methodFqn)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (visited.size() > 12) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (containsBooleanTernaryCondition(methodFqn, paramName, context)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
List<CallEdge> edges = callGraph.get(methodFqn);
|
||||||
|
if (edges == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (CallEdge edge : edges) {
|
||||||
|
if (edge.getTargetMethod() != null) {
|
||||||
|
queue.add(edge.getTargetMethod());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean containsBooleanTernaryCondition(String methodFqn, String paramName, CodebaseContext context) {
|
||||||
|
MethodDeclaration method = findMethodDeclaration(methodFqn, context);
|
||||||
|
if (method == null || method.getBody() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hasParameter(method, paramName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final boolean[] found = {false};
|
||||||
|
method.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ConditionalExpression node) {
|
||||||
|
if (isBooleanConditionForParameter(node.getExpression(), paramName)) {
|
||||||
|
found[0] = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
for (Object argObj : node.arguments()) {
|
||||||
|
if (argObj instanceof ConditionalExpression cond
|
||||||
|
&& isBooleanConditionForParameter(cond.getExpression(), paramName)) {
|
||||||
|
found[0] = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return found[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBooleanConditionForParameter(Expression condition, String paramName) {
|
||||||
|
if (condition instanceof SimpleName simpleName) {
|
||||||
|
return paramName.equals(simpleName.getIdentifier());
|
||||||
|
}
|
||||||
|
if (condition instanceof PrefixExpression prefixExpression
|
||||||
|
&& prefixExpression.getOperator() == PrefixExpression.Operator.NOT
|
||||||
|
&& prefixExpression.getOperand() instanceof SimpleName simpleName) {
|
||||||
|
return paramName.equals(simpleName.getIdentifier());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isPathOrQueryVariable(EntryPoint.Parameter parameter) {
|
private static boolean isPathOrQueryVariable(EntryPoint.Parameter parameter) {
|
||||||
if (parameter.getAnnotations() == null) {
|
if (parameter.getAnnotations() == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -471,9 +471,48 @@ public class GenericEventDetector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (enclosing instanceof TypeDeclaration typeDeclaration) {
|
||||||
|
for (MethodDeclaration method : typeDeclaration.getMethods()) {
|
||||||
|
if (!hasAutowiredAnnotation(method) || method.parameters().size() != 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SingleVariableDeclaration param = (SingleVariableDeclaration) method.parameters().get(0);
|
||||||
|
if (!matchesInjectionTarget(method, fieldName, param)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String qualifier = extractQualifierAnnotation(param.modifiers());
|
||||||
|
if (qualifier != null) {
|
||||||
|
return qualifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean hasAutowiredAnnotation(MethodDeclaration method) {
|
||||||
|
for (Object modifierObj : method.modifiers()) {
|
||||||
|
if (modifierObj instanceof Annotation annotation
|
||||||
|
&& annotation.getTypeName().toString().endsWith("Autowired")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean matchesInjectionTarget(
|
||||||
|
MethodDeclaration method, String fieldName, SingleVariableDeclaration param) {
|
||||||
|
if (fieldName.equals(param.getName().getIdentifier())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String methodName = method.getName().getIdentifier();
|
||||||
|
if (methodName.startsWith("set") && methodName.length() > 3) {
|
||||||
|
String propertyName = methodName.substring(3);
|
||||||
|
return propertyName.equalsIgnoreCase(fieldName)
|
||||||
|
|| (Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1)).equals(fieldName);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private String extractQualifierFromBinding(IVariableBinding binding) {
|
private String extractQualifierFromBinding(IVariableBinding binding) {
|
||||||
if (binding == null) {
|
if (binding == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -484,16 +523,31 @@ public class GenericEventDetector {
|
|||||||
}
|
}
|
||||||
if (binding.isField() && binding.getDeclaringClass() != null) {
|
if (binding.isField() && binding.getDeclaringClass() != null) {
|
||||||
for (IMethodBinding method : binding.getDeclaringClass().getDeclaredMethods()) {
|
for (IMethodBinding method : binding.getDeclaringClass().getDeclaredMethods()) {
|
||||||
if (!method.isConstructor()) {
|
boolean isAutowiredMethod = false;
|
||||||
|
for (IAnnotationBinding ann : method.getAnnotations()) {
|
||||||
|
if (ann.getAnnotationType() != null
|
||||||
|
&& "org.springframework.beans.factory.annotation.Autowired"
|
||||||
|
.equals(ann.getAnnotationType().getQualifiedName())) {
|
||||||
|
isAutowiredMethod = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!method.isConstructor() && !isAutowiredMethod) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < method.getParameterTypes().length; i++) {
|
for (int i = 0; i < method.getParameterTypes().length; i++) {
|
||||||
if (method.getParameterTypes()[i].isEqualTo(binding.getType().getErasure())
|
if (!method.getParameterTypes()[i].isEqualTo(binding.getType().getErasure())) {
|
||||||
&& binding.getName().equals(method.getParameterNames()[i])) {
|
continue;
|
||||||
String paramQualifier = readQualifierFromAnnotations(method.getParameterAnnotations(i));
|
}
|
||||||
if (paramQualifier != null) {
|
if (method.isConstructor() && !binding.getName().equals(method.getParameterNames()[i])) {
|
||||||
return paramQualifier;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!method.isConstructor() && !matchesSetterParameterName(method.getName(), binding.getName())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String paramQualifier = readQualifierFromAnnotations(method.getParameterAnnotations(i));
|
||||||
|
if (paramQualifier != null) {
|
||||||
|
return paramQualifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,6 +555,18 @@ public class GenericEventDetector {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean matchesSetterParameterName(String methodName, String fieldName) {
|
||||||
|
if (methodName == null || fieldName == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (methodName.startsWith("set") && methodName.length() > 3) {
|
||||||
|
String propertyName = methodName.substring(3);
|
||||||
|
return propertyName.equalsIgnoreCase(fieldName)
|
||||||
|
|| (Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1)).equals(fieldName);
|
||||||
|
}
|
||||||
|
return fieldName.equals(methodName);
|
||||||
|
}
|
||||||
|
|
||||||
private String readQualifierFromAnnotations(IAnnotationBinding[] annotations) {
|
private String readQualifierFromAnnotations(IAnnotationBinding[] annotations) {
|
||||||
if (annotations == null) {
|
if (annotations == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -172,6 +172,12 @@ public class GoldenUpdater {
|
|||||||
Path.of("src/test/resources/golden/ExtendedStateMachineConfig"),
|
Path.of("src/test/resources/golden/ExtendedStateMachineConfig"),
|
||||||
"ExtendedStateMachineConfig"
|
"ExtendedStateMachineConfig"
|
||||||
),
|
),
|
||||||
|
new TestScenario(
|
||||||
|
"Payment State Machine (Extended Sample)",
|
||||||
|
Path.of("state_machines/extended_analysis_sample"),
|
||||||
|
Path.of("src/test/resources/golden/PaymentStateMachineConfig"),
|
||||||
|
"PaymentStateMachineConfig"
|
||||||
|
),
|
||||||
new TestScenario(
|
new TestScenario(
|
||||||
"Extended Analysis Sample (PROD)",
|
"Extended Analysis Sample (PROD)",
|
||||||
Path.of("state_machines/extended_analysis_sample"),
|
Path.of("state_machines/extended_analysis_sample"),
|
||||||
|
|||||||
@@ -113,6 +113,12 @@ public class PlantUmlE2ETest {
|
|||||||
Path.of("src/test/resources/golden/ExtendedStateMachineConfig"),
|
Path.of("src/test/resources/golden/ExtendedStateMachineConfig"),
|
||||||
"ExtendedStateMachineConfig"
|
"ExtendedStateMachineConfig"
|
||||||
),
|
),
|
||||||
|
new TestScenario(
|
||||||
|
"Payment State Machine (Extended Sample)",
|
||||||
|
root.resolve("state_machines/extended_analysis_sample"),
|
||||||
|
Path.of("src/test/resources/golden/PaymentStateMachineConfig"),
|
||||||
|
"PaymentStateMachineConfig"
|
||||||
|
),
|
||||||
new TestScenario(
|
new TestScenario(
|
||||||
"Extended Analysis Sample (PROD)",
|
"Extended Analysis Sample (PROD)",
|
||||||
root.resolve("state_machines/extended_analysis_sample"),
|
root.resolve("state_machines/extended_analysis_sample"),
|
||||||
|
|||||||
@@ -113,6 +113,12 @@ public class RegressionTest {
|
|||||||
Path.of("src/test/resources/golden/ExtendedStateMachineConfig"),
|
Path.of("src/test/resources/golden/ExtendedStateMachineConfig"),
|
||||||
"ExtendedStateMachineConfig"
|
"ExtendedStateMachineConfig"
|
||||||
),
|
),
|
||||||
|
new TestScenario(
|
||||||
|
"Payment State Machine (Extended Sample)",
|
||||||
|
root.resolve("state_machines/extended_analysis_sample"),
|
||||||
|
Path.of("src/test/resources/golden/PaymentStateMachineConfig"),
|
||||||
|
"PaymentStateMachineConfig"
|
||||||
|
),
|
||||||
new TestScenario(
|
new TestScenario(
|
||||||
"Extended Analysis Sample (PROD)",
|
"Extended Analysis Sample (PROD)",
|
||||||
root.resolve("state_machines/extended_analysis_sample"),
|
root.resolve("state_machines/extended_analysis_sample"),
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
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 BooleanTernaryBranchSplitTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bindingExpanderShouldProduceBooleanVariantsForTernaryParameter(@TempDir Path tempDir) throws Exception {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class PolymorphicController {
|
||||||
|
OrderService orderService;
|
||||||
|
public void payTernary(boolean isPay) {
|
||||||
|
orderService.processEvent(isPay ? new PayEvent() : new CancelEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class OrderService {
|
||||||
|
void processEvent(BaseEvent event) {}
|
||||||
|
}
|
||||||
|
class BaseEvent {}
|
||||||
|
class PayEvent extends BaseEvent {}
|
||||||
|
class CancelEvent extends BaseEvent {}
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("App.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.PolymorphicController")
|
||||||
|
.methodName("payTernary")
|
||||||
|
.name("POST /pay-ternary")
|
||||||
|
.parameters(List.of(EntryPoint.Parameter.builder()
|
||||||
|
.name("isPay")
|
||||||
|
.type("boolean")
|
||||||
|
.build()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<Map<String, String>> variants = EntryPointBindingExpander.expandEntryPointBindings(
|
||||||
|
entryPoint, context, engine.buildCallGraph());
|
||||||
|
|
||||||
|
assertThat(variants).extracting(map -> map.get("isPay"))
|
||||||
|
.containsExactlyInAnyOrder("true", "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveSingleEventPerBooleanBinding(@TempDir Path tempDir) throws Exception {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class PolymorphicController {
|
||||||
|
OrderService orderService;
|
||||||
|
public void payTernary(boolean isPay) {
|
||||||
|
orderService.processEvent(isPay ? new PayEvent() : new CancelEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class OrderService {
|
||||||
|
void processEvent(BaseEvent event) {}
|
||||||
|
}
|
||||||
|
class BaseEvent {}
|
||||||
|
class PayEvent extends BaseEvent {}
|
||||||
|
class CancelEvent extends BaseEvent {}
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("App.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.PolymorphicController")
|
||||||
|
.methodName("payTernary")
|
||||||
|
.name("POST /pay-ternary")
|
||||||
|
.parameters(List.of(EntryPoint.Parameter.builder()
|
||||||
|
.name("isPay")
|
||||||
|
.type("boolean")
|
||||||
|
.build()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("processEvent")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
CallChain payChain = chains.stream()
|
||||||
|
.filter(c -> c.getEntryPoint().getName().contains("isPay=true"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
CallChain cancelChain = chains.stream()
|
||||||
|
.filter(c -> c.getEntryPoint().getName().contains("isPay=false"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
|
||||||
|
assertThat(payChain.getTriggerPoint().isAmbiguous()).isFalse();
|
||||||
|
assertThat(cancelChain.getTriggerPoint().isAmbiguous()).isFalse();
|
||||||
|
assertThat(payChain.getTriggerPoint().getEvent()).contains("PayEvent").doesNotContain("CancelEvent");
|
||||||
|
assertThat(cancelChain.getTriggerPoint().getEvent()).contains("CancelEvent").doesNotContain("PayEvent");
|
||||||
|
if (payChain.getTriggerPoint().getPolymorphicEvents() != null) {
|
||||||
|
assertThat(payChain.getTriggerPoint().getPolymorphicEvents()).hasSizeLessThanOrEqualTo(1);
|
||||||
|
}
|
||||||
|
if (cancelChain.getTriggerPoint().getPolymorphicEvents() != null) {
|
||||||
|
assertThat(cancelChain.getTriggerPoint().getPolymorphicEvents()).hasSizeLessThanOrEqualTo(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,6 +108,41 @@ class GenericEventDetectorFireTest {
|
|||||||
assertThat(triggers.get(0).getStateMachineId()).isEqualTo("paymentStateMachine");
|
assertThat(triggers.get(0).getStateMachineId()).isEqualTo("paymentStateMachine");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldInferStateMachineIdFromQualifierOnAutowiredSetter(@TempDir Path tempDir) throws Exception {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
enum OrderEvent { PAY }
|
||||||
|
class OrderService {
|
||||||
|
private StateMachine stateMachine;
|
||||||
|
@org.springframework.beans.factory.annotation.Autowired
|
||||||
|
public void setStateMachine(
|
||||||
|
@org.springframework.beans.factory.annotation.Qualifier("paymentStateMachine")
|
||||||
|
StateMachine stateMachine) {
|
||||||
|
this.stateMachine = stateMachine;
|
||||||
|
}
|
||||||
|
public void pay() {
|
||||||
|
stateMachine.sendEvent(OrderEvent.PAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class StateMachine {
|
||||||
|
public void sendEvent(OrderEvent event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
GenericEventDetector detector = new GenericEventDetector(context, context.getConstantResolver(), List.of());
|
||||||
|
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration("com.example.OrderService");
|
||||||
|
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
|
||||||
|
|
||||||
|
assertThat(triggers).hasSize(1);
|
||||||
|
assertThat(triggers.get(0).getStateMachineId()).isEqualTo("paymentStateMachine");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldNotInferSourceStateFromVariableOnlyIfCondition(@TempDir Path tempDir) throws Exception {
|
void shouldNotInferSourceStateFromVariableOnlyIfCondition(@TempDir Path tempDir) throws Exception {
|
||||||
String source = """
|
String source = """
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
digraph statemachine {
|
||||||
|
rankdir=LR;
|
||||||
|
node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
|
||||||
|
edge [fontname="Arial", fontsize=10];
|
||||||
|
|
||||||
|
_start [shape=circle, label="", fillcolor=black, width=0.1];
|
||||||
|
_start -> NEW;
|
||||||
|
DECLINED [fillcolor=lightgray];
|
||||||
|
QUIRK2 [fillcolor=lightgray];
|
||||||
|
QUIRK1 [fillcolor=lightgray];
|
||||||
|
QUIRK4 [fillcolor=lightgray];
|
||||||
|
QUIRK3 [fillcolor=lightgray];
|
||||||
|
CAPTURED [fillcolor=lightgray];
|
||||||
|
NEW -> AUTHORIZED [label="String.AUTHORIZE", style="solid", color="black"];
|
||||||
|
AUTHORIZED -> CAPTURED [label="String.CAPTURE", style="solid", color="black"];
|
||||||
|
NEW -> DECLINED [label="String.DECLINE", style="solid", color="black"];
|
||||||
|
NEW -> QUIRK1 [label="String.PRIMARY_EVENT", style="solid", color="black"];
|
||||||
|
NEW -> QUIRK2 [label="String.NAMED_EVENT", style="solid", color="black"];
|
||||||
|
NEW -> QUIRK3 [label="String.QUALIFIER_EVENT", style="solid", color="black"];
|
||||||
|
NEW -> QUIRK4 [label="String.FALLBACK_EVENT", style="solid", color="black"];
|
||||||
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,42 @@
|
|||||||
|
@startuml
|
||||||
|
!pragma layout smetana
|
||||||
|
set separator none
|
||||||
|
hide empty description
|
||||||
|
hide stereotype
|
||||||
|
skinparam state {
|
||||||
|
BackgroundColor white
|
||||||
|
BorderColor #94a3b8
|
||||||
|
BorderThickness 1
|
||||||
|
FontName Inter
|
||||||
|
FontSize 9
|
||||||
|
FontStyle bold
|
||||||
|
RoundCorner 20
|
||||||
|
Padding 1
|
||||||
|
}
|
||||||
|
skinparam shadowing false
|
||||||
|
skinparam ArrowFontName JetBrains Mono
|
||||||
|
skinparam ArrowFontSize 8
|
||||||
|
skinparam ArrowColor #cbd5e1
|
||||||
|
skinparam ArrowThickness 1
|
||||||
|
skinparam dpi 110
|
||||||
|
skinparam svgLinkTarget _self
|
||||||
|
|
||||||
|
[*] --> String.NEW
|
||||||
|
|
||||||
|
|
||||||
|
String.NEW -[#1E90FF,bold]-> String.AUTHORIZED <<external>> : String.AUTHORIZE
|
||||||
|
String.AUTHORIZED -[#1E90FF,bold]-> String.CAPTURED <<external>> : String.CAPTURE
|
||||||
|
String.NEW -[#1E90FF,bold]-> String.DECLINED <<external>> : String.DECLINE
|
||||||
|
String.NEW -[#1E90FF,bold]-> String.QUIRK1 <<external>> : String.PRIMARY_EVENT
|
||||||
|
String.NEW -[#1E90FF,bold]-> String.QUIRK2 <<external>> : String.NAMED_EVENT
|
||||||
|
String.NEW -[#1E90FF,bold]-> String.QUIRK3 <<external>> : String.QUALIFIER_EVENT
|
||||||
|
String.NEW -[#1E90FF,bold]-> String.QUIRK4 <<external>> : String.FALLBACK_EVENT
|
||||||
|
|
||||||
|
String.DECLINED --> [*]
|
||||||
|
String.QUIRK2 --> [*]
|
||||||
|
String.QUIRK1 --> [*]
|
||||||
|
String.QUIRK4 --> [*]
|
||||||
|
String.QUIRK3 --> [*]
|
||||||
|
String.CAPTURED --> [*]
|
||||||
|
@enduml
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="NEW">
|
||||||
|
<state id="NEW">
|
||||||
|
<transition target="AUTHORIZED" event="String.AUTHORIZE"/>
|
||||||
|
<transition target="DECLINED" event="String.DECLINE"/>
|
||||||
|
<transition target="QUIRK1" event="String.PRIMARY_EVENT"/>
|
||||||
|
<transition target="QUIRK2" event="String.NAMED_EVENT"/>
|
||||||
|
<transition target="QUIRK3" event="String.QUALIFIER_EVENT"/>
|
||||||
|
<transition target="QUIRK4" event="String.FALLBACK_EVENT"/>
|
||||||
|
</state>
|
||||||
|
<state id="AUTHORIZED">
|
||||||
|
<transition target="CAPTURED" event="String.CAPTURE"/>
|
||||||
|
</state>
|
||||||
|
<state id="CAPTURED">
|
||||||
|
</state>
|
||||||
|
<state id="DECLINED">
|
||||||
|
</state>
|
||||||
|
<state id="QUIRK1">
|
||||||
|
</state>
|
||||||
|
<state id="QUIRK2">
|
||||||
|
</state>
|
||||||
|
<state id="QUIRK3">
|
||||||
|
</state>
|
||||||
|
<state id="QUIRK4">
|
||||||
|
</state>
|
||||||
|
</scxml>
|
||||||
|
|
||||||
@@ -197,12 +197,27 @@
|
|||||||
"parameters" : [ ]
|
"parameters" : [ ]
|
||||||
}, {
|
}, {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
"name" : "POST /pay-ternary",
|
"name" : "POST /pay-ternary?isPay=true",
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
||||||
"methodName" : "payTernary",
|
"methodName" : "payTernary",
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
||||||
"metadata" : {
|
"metadata" : {
|
||||||
"path" : "/pay-ternary",
|
"path" : "/pay-ternary?isPay=true",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "isPay",
|
||||||
|
"type" : "boolean",
|
||||||
|
"annotations" : [ ]
|
||||||
|
} ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /pay-ternary?isPay=false",
|
||||||
|
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
||||||
|
"methodName" : "payTernary",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/pay-ternary?isPay=false",
|
||||||
"verb" : "POST"
|
"verb" : "POST"
|
||||||
},
|
},
|
||||||
"parameters" : [ {
|
"parameters" : [ {
|
||||||
@@ -479,12 +494,12 @@
|
|||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
"name" : "POST /pay-ternary",
|
"name" : "POST /pay-ternary?isPay=true",
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
||||||
"methodName" : "payTernary",
|
"methodName" : "payTernary",
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
||||||
"metadata" : {
|
"metadata" : {
|
||||||
"path" : "/pay-ternary",
|
"path" : "/pay-ternary?isPay=true",
|
||||||
"verb" : "POST"
|
"verb" : "POST"
|
||||||
},
|
},
|
||||||
"parameters" : [ {
|
"parameters" : [ {
|
||||||
@@ -495,22 +510,65 @@
|
|||||||
},
|
},
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payTernary", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payTernary", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
||||||
"triggerPoint" : {
|
"triggerPoint" : {
|
||||||
"event" : "isPay ? new PayEvent() : new CancelEvent()",
|
"event" : "new PayEvent()",
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
||||||
"methodName" : "processEvent",
|
"methodName" : "processEvent",
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.SUBMITTED",
|
||||||
"lineNumber" : 13,
|
"lineNumber" : 13,
|
||||||
"polymorphicEvents" : [ "click.kamil.examples.statemachine.polymorphic.OrderEvents.PAY", "click.kamil.examples.statemachine.polymorphic.OrderEvents.CANCEL" ],
|
"polymorphicEvents" : [ "click.kamil.examples.statemachine.polymorphic.OrderEvents.PAY" ],
|
||||||
"external" : false,
|
"external" : false,
|
||||||
"constraint" : null,
|
"constraint" : null,
|
||||||
"ambiguous" : true
|
"ambiguous" : false
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null,
|
"matchedTransitions" : [ {
|
||||||
"linkResolution" : "AMBIGUOUS_WIDEN"
|
"sourceState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.SUBMITTED",
|
||||||
|
"targetState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.PAID",
|
||||||
|
"event" : "click.kamil.examples.statemachine.polymorphic.OrderEvents.PAY"
|
||||||
|
} ],
|
||||||
|
"linkResolution" : "RESOLVED"
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /pay-ternary?isPay=false",
|
||||||
|
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
||||||
|
"methodName" : "payTernary",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/pay-ternary?isPay=false",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "isPay",
|
||||||
|
"type" : "boolean",
|
||||||
|
"annotations" : [ ]
|
||||||
|
} ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payTernary", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "new CancelEvent()",
|
||||||
|
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
||||||
|
"methodName" : "processEvent",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.SUBMITTED",
|
||||||
|
"lineNumber" : 13,
|
||||||
|
"polymorphicEvents" : [ "click.kamil.examples.statemachine.polymorphic.OrderEvents.CANCEL" ],
|
||||||
|
"external" : false,
|
||||||
|
"constraint" : null,
|
||||||
|
"ambiguous" : false
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : [ {
|
||||||
|
"sourceState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.SUBMITTED",
|
||||||
|
"targetState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.CANCELED",
|
||||||
|
"event" : "click.kamil.examples.statemachine.polymorphic.OrderEvents.CANCEL"
|
||||||
|
} ],
|
||||||
|
"linkResolution" : "RESOLVED"
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
|
|||||||
Reference in New Issue
Block a user