This commit is contained in:
2026-07-17 12:38:45 +02:00
parent 6909af8727
commit 9a3b0403a7
26 changed files with 1816 additions and 159 deletions

View File

@@ -69,6 +69,7 @@ public class CallChainEnricher implements AnalysisEnricher {
}
List<CallChain> chains = intelligence.findCallChains(entryPoints, scopedTriggers);
// Semantic deduplication runs in TransitionLinkerEnricher after transitions are linked.
return MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
}

View File

@@ -0,0 +1,271 @@
package click.kamil.springstatemachineexporter.analysis.enricher;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Collapses call chains that represent the same logical entrypoint-to-transition link
* but were discovered via different call-graph paths.
*/
public final class SemanticCallChainDedup {
public enum KeepStrategy {
SHORTEST,
FIRST
}
private SemanticCallChainDedup() {
}
public static List<CallChain> deduplicate(List<CallChain> chains) {
return deduplicate(chains, KeepStrategy.SHORTEST);
}
public static List<CallChain> deduplicate(List<CallChain> chains, KeepStrategy strategy) {
if (chains == null || chains.isEmpty()) {
return chains == null ? List.of() : chains;
}
Map<String, CallChain> byKey = new LinkedHashMap<>();
for (CallChain chain : chains) {
if (chain == null) {
continue;
}
String key = semanticKey(chain);
CallChain existing = byKey.get(key);
if (existing == null) {
byKey.put(key, chain);
} else {
byKey.put(key, pickPreferred(existing, chain, strategy));
}
}
return new ArrayList<>(byKey.values());
}
public static List<CallChain> merge(
List<CallChain> existing,
List<CallChain> refreshed,
KeepStrategy strategy) {
List<CallChain> combined = new ArrayList<>();
if (existing != null) {
combined.addAll(existing);
}
if (refreshed != null) {
combined.addAll(refreshed);
}
return deduplicate(combined, strategy);
}
/**
* JSON refresh path: when refresh produces any chains for an entry point, those replace all
* existing chains for that entry (so unresolved stubs like {@code event} do not linger).
* Multiple refreshed chains per entry (multi-event fan-out) are preserved, then semantic
* dedup collapses path variants.
*/
public static List<CallChain> mergeRefresh(
List<CallChain> existing,
List<CallChain> refreshed,
KeepStrategy strategy) {
Map<String, List<CallChain>> existingByEntry = groupByEntryPoint(existing);
Map<String, List<CallChain>> refreshedByEntry = groupByEntryPoint(refreshed);
LinkedHashSet<String> entries = new LinkedHashSet<>();
entries.addAll(existingByEntry.keySet());
entries.addAll(refreshedByEntry.keySet());
List<CallChain> combined = new ArrayList<>();
for (String entryKey : entries) {
if (refreshedByEntry.containsKey(entryKey)) {
combined.addAll(refreshedByEntry.get(entryKey));
} else {
combined.addAll(existingByEntry.getOrDefault(entryKey, List.of()));
}
}
return deduplicate(combined, strategy);
}
private static Map<String, List<CallChain>> groupByEntryPoint(List<CallChain> chains) {
Map<String, List<CallChain>> byEntry = new LinkedHashMap<>();
if (chains == null) {
return byEntry;
}
for (CallChain chain : chains) {
if (chain == null) {
continue;
}
byEntry.computeIfAbsent(entryPointKey(chain), key -> new ArrayList<>()).add(chain);
}
return byEntry;
}
static String semanticKey(CallChain chain) {
StringBuilder key = new StringBuilder();
key.append(entryPointKey(chain)).append('|');
key.append(triggerKey(chain.getTriggerPoint())).append('|');
key.append(linkResolutionKey(chain.getLinkResolution())).append('|');
key.append(matchedTransitionsKey(chain.getMatchedTransitions())).append('|');
key.append(nullToEmpty(chain.getContextMachineId()));
return key.toString();
}
private static CallChain pickPreferred(CallChain existing, CallChain candidate, KeepStrategy strategy) {
if (strategy == KeepStrategy.FIRST) {
return existing;
}
boolean existingAnchored = startsAtEntryPoint(existing);
boolean candidateAnchored = startsAtEntryPoint(candidate);
if (candidateAnchored != existingAnchored) {
return candidateAnchored ? candidate : existing;
}
int existingLen = chainLength(existing);
int candidateLen = chainLength(candidate);
if (candidateLen < existingLen) {
return candidate;
}
if (candidateLen > existingLen) {
return existing;
}
int existingRichness = metadataRichness(existing);
int candidateRichness = metadataRichness(candidate);
if (candidateRichness != existingRichness) {
return candidateRichness > existingRichness ? candidate : existing;
}
List<String> existingChain = existing.getMethodChain();
List<String> candidateChain = candidate.getMethodChain();
if (existingChain == null) {
return candidate;
}
if (candidateChain == null) {
return existing;
}
return candidateChain.toString().compareTo(existingChain.toString()) < 0 ? candidate : existing;
}
/**
* True when the first hop equals the entry point method identity
* ({@code className.methodName} or {@code className#methodName} key form).
*/
static boolean startsAtEntryPoint(CallChain chain) {
if (chain == null) {
return false;
}
EntryPoint entryPoint = chain.getEntryPoint();
List<String> methodChain = chain.getMethodChain();
if (entryPoint == null
|| entryPoint.getClassName() == null
|| entryPoint.getMethodName() == null
|| methodChain == null
|| methodChain.isEmpty()) {
return false;
}
String firstHop = methodChain.get(0);
if (firstHop == null || firstHop.isBlank()) {
return false;
}
String entryHop = entryPoint.getClassName() + "." + entryPoint.getMethodName();
if (entryHop.equals(firstHop)) {
return true;
}
return normalizeMethodHopToEntryKey(firstHop)
.equals(entryPoint.getClassName() + "#" + entryPoint.getMethodName());
}
private static int metadataRichness(CallChain chain) {
int score = 0;
if (chain.getEntryPoint() != null) {
score += 2;
}
if (chain.getMethodChain() != null && !chain.getMethodChain().isEmpty()) {
score += 1;
}
if (chain.getMatchedTransitions() != null && !chain.getMatchedTransitions().isEmpty()) {
score += 4;
}
return score;
}
private static int chainLength(CallChain chain) {
return chain.getMethodChain() == null ? Integer.MAX_VALUE : chain.getMethodChain().size();
}
private static String entryPointKey(CallChain chain) {
EntryPoint entryPoint = chain.getEntryPoint();
if (entryPoint != null) {
if (entryPoint.getName() != null && !entryPoint.getName().isBlank()) {
return entryPoint.getName();
}
if (entryPoint.getClassName() != null && entryPoint.getMethodName() != null) {
return entryPoint.getClassName() + "#" + entryPoint.getMethodName();
}
}
List<String> methodChain = chain.getMethodChain();
if (methodChain != null && !methodChain.isEmpty()) {
return normalizeMethodHopToEntryKey(methodChain.get(0));
}
TriggerPoint trigger = chain.getTriggerPoint();
if (trigger != null && trigger.getClassName() != null && trigger.getMethodName() != null) {
return trigger.getClassName() + "#" + trigger.getMethodName();
}
return "";
}
private static String normalizeMethodHopToEntryKey(String hop) {
if (hop == null || hop.isBlank()) {
return "";
}
int lastDot = hop.lastIndexOf('.');
if (lastDot <= 0 || lastDot >= hop.length() - 1) {
return hop;
}
return hop.substring(0, lastDot) + "#" + hop.substring(lastDot + 1);
}
private static String triggerKey(TriggerPoint trigger) {
if (trigger == null) {
return "";
}
StringBuilder key = new StringBuilder();
key.append(nullToEmpty(trigger.getClassName())).append('#');
key.append(nullToEmpty(trigger.getMethodName())).append('|');
key.append(nullToEmpty(trigger.getEvent())).append('|');
key.append(nullToEmpty(trigger.getConstraint())).append('|');
key.append(trigger.isExternal()).append('|');
key.append(trigger.isAmbiguous()).append('|');
key.append(nullToEmpty(trigger.getSourceState()));
return key.toString();
}
private static String linkResolutionKey(LinkResolution resolution) {
return resolution == null ? "" : resolution.name();
}
private static String matchedTransitionsKey(List<MatchedTransition> matchedTransitions) {
if (matchedTransitions == null || matchedTransitions.isEmpty()) {
return "";
}
return matchedTransitions.stream()
.filter(Objects::nonNull)
.sorted(Comparator
.comparing((MatchedTransition mt) -> nullToEmpty(mt.getEvent()))
.thenComparing(mt -> nullToEmpty(mt.getSourceState()))
.thenComparing(mt -> nullToEmpty(mt.getTargetState())))
.map(mt -> nullToEmpty(mt.getEvent()) + ">"
+ nullToEmpty(mt.getSourceState()) + ">"
+ nullToEmpty(mt.getTargetState()))
.collect(Collectors.joining(","));
}
private static String nullToEmpty(String value) {
return value == null ? "" : value;
}
}

View File

@@ -243,6 +243,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
result.getMetadata().getTriggers(), result.getName(), context, stateMachineTransitions);
List<CallChain> scopedChains = MachineScopeFilter.filterCallChainsForMachine(
updatedChains, result.getName(), context, stateMachineTransitions);
scopedChains = SemanticCallChainDedup.deduplicate(scopedChains);
List<EntryPoint> scopedEntryPoints = EntryPointScopeResolver.scopeFromCallChains(
scopedChains, result.getMetadata().getEntryPoints());
@@ -431,11 +432,20 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
== MachineEnumCanonicalizer.TriggerEventKind.CANONICAL_ENUM;
}
/**
* Prefer a concrete trigger type; otherwise adopt the machine type only when it is also
* concrete. Non-concrete machine types ({@code String}/{@code Object}/type-vars) must not be
* stamped onto triggers — that makes multi-machine affinity treat them as ambiguous shared
* generics and fail-closed, wiping package-proven String-machine chains (e.g. MavenOrder JMS).
*/
private static String concreteOrFallback(String triggerTypeFqn, String machineTypeFqn) {
if (SharedServiceRoutingPolicy.isConcreteMachineType(triggerTypeFqn)) {
return triggerTypeFqn;
}
if (SharedServiceRoutingPolicy.isConcreteMachineType(machineTypeFqn)) {
return machineTypeFqn;
}
return triggerTypeFqn;
}
}

View File

@@ -105,9 +105,17 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
return isStringTypeOrPrimitive(smEnumType);
}
// Prevent matching enum triggers to string transitions
if (!smEvent.contains(".") && triggerEvent.contains(".")) {
return false;
// Trigger scraped as String.CONST while config still stores the bare literal constant.
if (isStringTypeOrPrimitive(eventTypeFqn) && triggerEvent.contains(".") && !smEvent.contains(".")) {
String triggerEnumType = triggerEvent.substring(0, triggerEvent.lastIndexOf('.'));
return isStringTypeOrPrimitive(triggerEnumType);
}
// Qualified trigger against bare SM event without eventTypeFqn: only when the qualifier
// itself is a string/primitive type (not a real enum like OrderEvent.PAY ↔ PAY).
if (eventTypeFqn == null && !smEvent.contains(".") && triggerEvent.contains(".")) {
String triggerEnumType = triggerEvent.substring(0, triggerEvent.lastIndexOf('.'));
return isStringTypeOrPrimitive(triggerEnumType);
}
if (smEvent.contains(".")) {

View File

@@ -964,6 +964,9 @@ public final class MachineEnumCanonicalizer {
}
if (typePart == null && Character.isUpperCase(stripped.charAt(0)) && !stripped.contains(".")) {
if (isStringOrPrimitiveType(enumTypeFqn)) {
return enumTypeFqn + "." + stripped;
}
// Only invent enumType.CONST when CONST is a real member — otherwise JMS payload
// labels like "PAY" become OrderState.PAY and break transition linking.
if (context != null && !enumHasConstant(enumTypeFqn, stripped, context)) {

View File

@@ -66,7 +66,31 @@ public final class CarrierConstraintSupport {
if (carrierBindings.isEmpty()) {
return false;
}
return !BooleanConstraintEvaluator.isCompatibleWithKnownBindings(constraint, carrierBindings);
String carrierConstraint = carrierOnlyConstraint(constraint, carrierBindings.keySet());
if (carrierConstraint == null || carrierConstraint.isBlank()) {
return false;
}
return !BooleanConstraintEvaluator.isCompatibleWithKnownBindings(carrierConstraint, carrierBindings);
}
/**
* Keeps AND-clauses whose primary identifier overlaps {@code carrierKeys}; machine-domain
* discriminators and unrelated locals are dropped so carrier binding checks are not skewed
* by if-else negations on other parameters.
*/
static String carrierOnlyConstraint(String constraint, Set<String> carrierKeys) {
if (constraint == null || constraint.isBlank() || carrierKeys == null || carrierKeys.isEmpty()) {
return "";
}
List<String> kept = new ArrayList<>();
for (String clause : ConstraintClauses.splitAndClauses(constraint)) {
String ident = ConstraintClauses.primaryIdent(clause);
if (ident == null || !containsIgnoreCase(carrierKeys, ident)) {
continue;
}
kept.add(clause.trim());
}
return joinAndTidy(kept);
}
private static Set<String> carrierPrimaryIdents(String constraint) {

View File

@@ -711,6 +711,65 @@ public class ConstructorAnalyzer {
}
}
/**
* Maps an inherited field supplied through {@code super(...)} to a subclass constructor parameter type.
* Returns null when the mapping cannot be proven.
*/
public String resolveSubclassConstructorParameterFqnForInheritedField(
TypeDeclaration subclassTd,
String inheritedFieldName,
click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer injectionAnalyzer) {
if (subclassTd == null || inheritedFieldName == null || inheritedFieldName.isBlank()) {
return null;
}
for (MethodDeclaration ctor : subclassTd.getMethods()) {
if (!ctor.isConstructor() || ctor.getBody() == null) {
continue;
}
int paramIdx = findAssignedParameterIndex(ctor, inheritedFieldName, context, new HashSet<>());
if (paramIdx < 0 || paramIdx >= ctor.parameters().size()) {
continue;
}
SingleVariableDeclaration param = (SingleVariableDeclaration) ctor.parameters().get(paramIdx);
IVariableBinding paramBinding = param.resolveBinding();
if (injectionAnalyzer != null && paramBinding != null) {
String injectedBean = injectionAnalyzer.resolveInjectedBeanFqn(paramBinding);
if (injectedBean != null && !injectedBean.isBlank()) {
return injectedBean;
}
}
if (paramBinding != null && paramBinding.getType() != null) {
String fqn = paramBinding.getType().getErasure().getQualifiedName();
if (fqn != null && !fqn.isBlank() && !"java.lang.Object".equals(fqn)) {
return fqn;
}
}
CompilationUnit cu = subclassTd.getRoot() instanceof CompilationUnit root ? root : null;
String astFqn = resolveTypeToFqn(param.getType(), cu);
if (astFqn != null && !astFqn.isBlank()) {
return astFqn;
}
}
return null;
}
/** Visible for unit tests verifying constructor super() parameter mapping. */
int findAssignedParameterIndexForTest(MethodDeclaration constructorMd, String fieldName) {
return findAssignedParameterIndex(constructorMd, fieldName, context, new HashSet<>());
}
private String resolveTypeToFqn(Type type, CompilationUnit cu) {
if (type == null) {
return null;
}
String raw = type.toString();
if (raw.contains("<")) {
raw = raw.substring(0, raw.indexOf('<'));
}
TypeDeclaration td = cu != null ? context.getTypeDeclaration(raw, cu) : context.getTypeDeclaration(raw);
return td != null ? context.getFqn(td) : raw;
}
private int findAssignedParameterIndex(MethodDeclaration constructorMd, String fieldName, CodebaseContext context, Set<MethodDeclaration> visited) {
if (constructorMd == null || constructorMd.getBody() == null) return -1;
if (!visited.add(constructorMd)) return -1;
@@ -738,67 +797,31 @@ public class ConstructorAnalyzer {
}
@Override
public boolean visit(ConstructorInvocation node) {
IMethodBinding resolvedBinding = node.resolveConstructorBinding();
TypeDeclaration enclosingTd = findEnclosingType(node);
if (enclosingTd != null) {
for (MethodDeclaration otherMd : enclosingTd.getMethods()) {
boolean matches = false;
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
} else {
matches = otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size();
}
if (matches) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
Expression arg = (Expression) node.arguments().get(targetIdx);
String argName = extractVariableName(arg);
if (argName != null) {
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(argName)) {
foundIdx[0] = i;
}
}
}
}
}
}
MethodDeclaration targetCtor = selectDelegatedConstructor(
enclosingTd.getMethods(),
node.resolveConstructorBinding(),
node.arguments().size(),
constructorMd);
mapDelegatedConstructorArgument(targetCtor, node.arguments(), fieldName, constructorMd, foundIdx, visited);
}
return super.visit(node);
}
@Override
public boolean visit(SuperConstructorInvocation node) {
IMethodBinding resolvedBinding = node.resolveConstructorBinding();
TypeDeclaration enclosingTd = findEnclosingType(node);
if (enclosingTd != null) {
String superFqn = context.getSuperclassFqn(enclosingTd);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
for (MethodDeclaration otherMd : superTd.getMethods()) {
boolean matches = false;
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
} else {
matches = otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size();
}
if (matches) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
Expression arg = (Expression) node.arguments().get(targetIdx);
String argName = extractVariableName(arg);
if (argName != null) {
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(argName)) {
foundIdx[0] = i;
}
}
}
}
}
}
MethodDeclaration targetCtor = selectDelegatedConstructor(
superTd.getMethods(),
node.resolveConstructorBinding(),
node.arguments().size(),
null);
mapDelegatedConstructorArgument(targetCtor, node.arguments(), fieldName, constructorMd, foundIdx, visited);
}
}
}
@@ -808,6 +831,97 @@ public class ConstructorAnalyzer {
return foundIdx[0];
}
/**
* Prefer JDT binding identity when available. If the binding is missing or does not map to a
* source constructor, fall back to a unique same-arity constructor. Ambiguous same-arity
* overloads without a binding match return null (do not guess).
*/
private static MethodDeclaration selectDelegatedConstructor(
MethodDeclaration[] candidates,
IMethodBinding resolvedBinding,
int argumentCount,
MethodDeclaration exclude) {
if (candidates == null) {
return null;
}
if (resolvedBinding != null) {
for (MethodDeclaration otherMd : candidates) {
if (!otherMd.isConstructor() || otherMd == exclude) {
continue;
}
IMethodBinding otherBinding = otherMd.resolveBinding();
if (otherBinding != null && constructorBindingsMatch(resolvedBinding, otherBinding)) {
return otherMd;
}
}
}
MethodDeclaration arityMatch = null;
for (MethodDeclaration otherMd : candidates) {
if (!otherMd.isConstructor() || otherMd == exclude) {
continue;
}
if (otherMd.parameters().size() != argumentCount) {
continue;
}
if (arityMatch != null) {
// Ambiguous same-arity overloads without a binding hit — do not guess.
return null;
}
arityMatch = otherMd;
}
return arityMatch;
}
private static boolean constructorBindingsMatch(IMethodBinding left, IMethodBinding right) {
if (left.isEqualTo(right) || Objects.equals(left.getKey(), right.getKey())) {
return true;
}
if (!Objects.equals(left.getName(), right.getName())) {
return false;
}
ITypeBinding[] leftParams = left.getParameterTypes();
ITypeBinding[] rightParams = right.getParameterTypes();
if (leftParams.length != rightParams.length) {
return false;
}
for (int i = 0; i < leftParams.length; i++) {
if (!leftParams[i].getErasure().isEqualTo(rightParams[i].getErasure())) {
return false;
}
}
ITypeBinding leftDecl = left.getDeclaringClass();
ITypeBinding rightDecl = right.getDeclaringClass();
return leftDecl != null && rightDecl != null && leftDecl.getErasure().isEqualTo(rightDecl.getErasure());
}
private void mapDelegatedConstructorArgument(
MethodDeclaration targetCtor,
List<?> arguments,
String fieldName,
MethodDeclaration constructorMd,
int[] foundIdx,
Set<MethodDeclaration> visited) {
if (targetCtor == null || arguments == null) {
return;
}
int targetIdx = findAssignedParameterIndex(targetCtor, fieldName, context, visited);
if (targetIdx < 0 || targetIdx >= arguments.size()) {
return;
}
Expression arg = (Expression) arguments.get(targetIdx);
String argName = extractVariableName(arg);
if (argName == null) {
return;
}
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(argName)) {
foundIdx[0] = i;
return;
}
}
}
private String extractVariableName(Expression expr) {
if (expr instanceof SimpleName sn) return sn.getIdentifier();
if (expr instanceof FieldAccess fa) return fa.getName().getIdentifier();

View File

@@ -0,0 +1,94 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Narrows generic superclass collaborator fields to the concrete type supplied by a subclass
* constructor via {@code super(...)} delegation.
*/
public final class ConstructorInjectionTypeNarrower {
private ConstructorInjectionTypeNarrower() {
}
public static String resolveConcreteReceiverTypeFqn(
IVariableBinding fieldBinding,
TypeDeclaration callSiteType,
CodebaseContext context,
InjectionPointAnalyzer injectionAnalyzer,
ConstructorAnalyzer constructorAnalyzer) {
if (fieldBinding == null
|| !fieldBinding.isField()
|| context == null
|| constructorAnalyzer == null
|| fieldBinding.getDeclaringClass() == null) {
return null;
}
String fieldName = fieldBinding.getName();
String declaringFqn = fieldBinding.getDeclaringClass().getErasure().getQualifiedName();
if (declaringFqn == null || declaringFqn.isBlank() || fieldName == null || fieldName.isBlank()) {
return null;
}
Set<String> concreteTypes = new LinkedHashSet<>();
for (String candidateFqn : candidateSubclassFqns(callSiteType, declaringFqn, context)) {
TypeDeclaration candidateTd = context.getTypeDeclaration(candidateFqn);
if (candidateTd == null || context.isSubtypeOrSame(candidateFqn, declaringFqn) == false) {
continue;
}
if (candidateTd.isInterface() || org.eclipse.jdt.core.dom.Modifier.isAbstract(candidateTd.getModifiers())) {
continue;
}
String narrowed = constructorAnalyzer.resolveSubclassConstructorParameterFqnForInheritedField(
candidateTd, fieldName, injectionAnalyzer);
if (narrowed != null && !narrowed.isBlank()) {
concreteTypes.add(narrowed);
}
}
return concreteTypes.size() == 1 ? concreteTypes.iterator().next() : null;
}
private static Set<String> candidateSubclassFqns(
TypeDeclaration callSiteType,
String declaringFqn,
CodebaseContext context) {
Set<String> candidates = new LinkedHashSet<>();
if (callSiteType != null) {
String callSiteFqn = context.getFqn(callSiteType);
if (callSiteFqn != null && !callSiteFqn.isBlank() && !callSiteFqn.equals(declaringFqn)) {
candidates.add(callSiteFqn);
}
}
List<String> implementations = context.getImplementations(declaringFqn);
if (implementations != null) {
candidates.addAll(implementations);
}
return candidates;
}
public static String resolveConcreteReceiverTypeFqnFromBinding(
IVariableBinding fieldBinding,
ITypeBinding callSiteTypeBinding,
CodebaseContext context,
InjectionPointAnalyzer injectionAnalyzer,
ConstructorAnalyzer constructorAnalyzer) {
if (callSiteTypeBinding == null) {
return resolveConcreteReceiverTypeFqn(
fieldBinding, null, context, injectionAnalyzer, constructorAnalyzer);
}
String callSiteFqn = callSiteTypeBinding.getErasure().getQualifiedName();
TypeDeclaration callSiteTd = context != null ? context.getTypeDeclaration(callSiteFqn) : null;
return resolveConcreteReceiverTypeFqn(
fieldBinding, callSiteTd, context, injectionAnalyzer, constructorAnalyzer);
}
}

View File

@@ -3,17 +3,12 @@ package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.CallChainEnricher;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerCanonicalizationEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.SemanticCallChainDedup;
import click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
@@ -83,7 +78,10 @@ public class EnrichmentService {
if (result.getMetadata() != null && context != null && intelligence != null) {
List<CallChain> refreshed = CallChainEnricher.buildCallChainsForMachine(result, context, intelligence);
if (refreshed != null) {
List<CallChain> merged = mergeCallChainsByEntryPoint(result.getMetadata().getCallChains(), refreshed);
List<CallChain> merged = SemanticCallChainDedup.mergeRefresh(
result.getMetadata().getCallChains(),
refreshed,
SemanticCallChainDedup.KeepStrategy.SHORTEST);
result.setMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
.triggers(result.getMetadata().getTriggers())
.entryPoints(result.getMetadata().getEntryPoints())
@@ -95,47 +93,6 @@ public class EnrichmentService {
relinkAfterPropertyResolution(result, context, intelligence);
}
private static List<CallChain> mergeCallChainsByEntryPoint(
List<CallChain> existing,
List<CallChain> refreshed) {
Map<String, CallChain> byEntryPoint = new LinkedHashMap<>();
if (existing != null) {
for (CallChain chain : existing) {
byEntryPoint.putIfAbsent(callChainKey(chain), chain);
}
}
for (CallChain chain : refreshed) {
byEntryPoint.put(callChainKey(chain), chain);
}
return new ArrayList<>(byEntryPoint.values());
}
private static String callChainKey(CallChain chain) {
if (chain == null) {
return "";
}
String entryPointKey = entryPointKey(chain.getEntryPoint());
if (!entryPointKey.isBlank()) {
return entryPointKey;
}
if (chain.getTriggerPoint() != null
&& chain.getTriggerPoint().getClassName() != null
&& chain.getTriggerPoint().getMethodName() != null) {
return chain.getTriggerPoint().getClassName() + "#" + chain.getTriggerPoint().getMethodName();
}
return "";
}
private static String entryPointKey(EntryPoint entryPoint) {
if (entryPoint == null) {
return "";
}
if (entryPoint.getName() != null && !entryPoint.getName().isBlank()) {
return entryPoint.getName();
}
return entryPoint.getClassName() + "#" + entryPoint.getMethodName();
}
private static void runEnrichers(
List<AnalysisEnricher> enrichers,
AnalysisResult result,

View File

@@ -79,6 +79,14 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
log.debug("CALLGRAPH RESOLVING BEAN FOR BINDING: {} calling {}", varBinding.getName(), methodName);
}
String concreteFqn = injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
if (concreteFqn == null) {
concreteFqn = ConstructorInjectionTypeNarrower.resolveConcreteReceiverTypeFqn(
varBinding,
enclosingType,
context,
injectionAnalyzer,
constructorAnalyzer);
}
if (concreteFqn != null) {
if (log.isDebugEnabled()) {
log.debug(" -> RESOLVED CONCRETE FQN: {}", concreteFqn);
@@ -153,7 +161,17 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
if (!(nameBinding instanceof IVariableBinding varBinding)) {
return null;
}
return injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
String direct = injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
if (direct != null) {
return direct;
}
TypeDeclaration enclosingType = findEnclosingType(receiver);
return ConstructorInjectionTypeNarrower.resolveConcreteReceiverTypeFqn(
varBinding,
enclosingType,
context,
injectionAnalyzer,
constructorAnalyzer);
}
private Expression unwrapMessageBuilder(Expression expr) {

View File

@@ -0,0 +1,386 @@
package click.kamil.springstatemachineexporter.analysis.enricher;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class SemanticCallChainDedupTest {
@Test
void shouldPreferEntryAnchoredPathOverShorterOrphanPath() {
EntryPoint entryPoint = EntryPoint.builder()
.type(EntryPoint.Type.JMS)
.name("JMS: order.queue")
.className("com.example.JmsOrderListener")
.methodName("onMessage")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("sendEvent")
.event("ORDER_EVENT")
.build();
List<MatchedTransition> matched = List.of(MatchedTransition.builder()
.event("ORDER_EVENT")
.sourceState("BUSY")
.targetState("DONE")
.build());
CallChain anchoredLonger = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(matched)
.methodChain(List.of(
"com.example.JmsOrderListener.onMessage",
"com.example.OrderService.onMessage",
"com.example.StateMachine.sendEvent"))
.build();
CallChain orphanShorter = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(matched)
.methodChain(List.of("com.example.OrderService.onMessage"))
.build();
List<CallChain> deduped = SemanticCallChainDedup.deduplicate(List.of(orphanShorter, anchoredLonger));
assertThat(deduped).hasSize(1);
assertThat(deduped.get(0).getMethodChain())
.containsExactly(
"com.example.JmsOrderListener.onMessage",
"com.example.OrderService.onMessage",
"com.example.StateMachine.sendEvent");
}
@Test
void shouldCollapseChainsWithSameSemanticKeyButDifferentPaths() {
EntryPoint entryPoint = EntryPoint.builder()
.type(EntryPoint.Type.REST)
.name("POST /api/order/pay")
.className("com.example.Api")
.methodName("pay")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("sendEvent")
.event("PAY")
.build();
List<MatchedTransition> matched = List.of(MatchedTransition.builder()
.event("PAY")
.sourceState("NEW")
.targetState("PAID")
.build());
CallChain longPath = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(matched)
.methodChain(List.of("a", "b", "c", "d"))
.build();
CallChain shortPath = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(matched)
.methodChain(List.of("a", "b"))
.build();
List<CallChain> deduped = SemanticCallChainDedup.deduplicate(List.of(longPath, shortPath));
assertThat(deduped).hasSize(1);
assertThat(deduped.get(0).getMethodChain()).containsExactly("a", "b");
}
@Test
void shouldKeepDistinctChainsWhenMatchedTransitionsDiffer() {
EntryPoint entryPoint = EntryPoint.builder()
.name("POST /api/order")
.className("com.example.Api")
.methodName("handle")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("sendEvent")
.event("PAY")
.build();
CallChain payChain = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(List.of(MatchedTransition.builder()
.event("PAY")
.sourceState("NEW")
.targetState("PAID")
.build()))
.methodChain(List.of("hop1"))
.build();
CallChain shipChain = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(List.of(MatchedTransition.builder()
.event("SHIP")
.sourceState("PAID")
.targetState("SHIPPED")
.build()))
.methodChain(List.of("hop1"))
.build();
List<CallChain> deduped = SemanticCallChainDedup.deduplicate(List.of(payChain, shipChain));
assertThat(deduped).hasSize(2);
}
@Test
void firstStrategyShouldKeepFirstSeenChain() {
EntryPoint entryPoint = EntryPoint.builder()
.name("JMS: orders")
.className("com.example.Listener")
.methodName("onMessage")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("sendEvent")
.event("PAY")
.build();
List<MatchedTransition> matched = List.of(MatchedTransition.builder()
.event("PAY")
.sourceState("NEW")
.targetState("PAID")
.build());
CallChain first = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(matched)
.methodChain(List.of("long", "path"))
.build();
CallChain second = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(matched)
.methodChain(List.of("short"))
.build();
List<CallChain> deduped = SemanticCallChainDedup.deduplicate(
List.of(first, second),
SemanticCallChainDedup.KeepStrategy.FIRST);
assertThat(deduped).hasSize(1);
assertThat(deduped.get(0).getMethodChain()).containsExactly("long", "path");
}
@Test
void mergeRefreshShouldReplaceExistingChainForSameEntryPoint() {
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.web.OrderController")
.methodName("pay")
.build();
CallChain existing = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(TriggerPoint.builder()
.className("com.example.web.OrderStateMachine")
.methodName("sendEvent")
.event("event")
.build())
.methodChain(null)
.build();
CallChain refreshed = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(TriggerPoint.builder()
.className("com.example.web.OrderController")
.methodName("pay")
.event("PAY")
.build())
.methodChain(List.of("com.example.web.OrderController.pay"))
.build();
List<CallChain> merged = SemanticCallChainDedup.mergeRefresh(
List.of(existing),
List.of(refreshed),
SemanticCallChainDedup.KeepStrategy.SHORTEST);
assertThat(merged).hasSize(1);
assertThat(merged.get(0).getMethodChain()).containsExactly("com.example.web.OrderController.pay");
assertThat(merged.get(0).getTriggerPoint().getEvent()).isEqualTo("PAY");
}
@Test
void mergeRefreshShouldReplaceSameEntryAndTriggerEvent() {
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.web.OrderController")
.methodName("pay")
.build();
CallChain existing = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(TriggerPoint.builder()
.className("com.example.web.OrderStateMachine")
.methodName("sendEvent")
.event("PAY")
.build())
.methodChain(null)
.build();
CallChain refreshed = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(TriggerPoint.builder()
.className("com.example.web.OrderController")
.methodName("pay")
.event("PAY")
.build())
.methodChain(List.of("com.example.web.OrderController.pay"))
.build();
List<CallChain> merged = SemanticCallChainDedup.mergeRefresh(
List.of(existing),
List.of(refreshed),
SemanticCallChainDedup.KeepStrategy.SHORTEST);
assertThat(merged).hasSize(1);
assertThat(merged.get(0).getMethodChain()).containsExactly("com.example.web.OrderController.pay");
assertThat(merged.get(0).getTriggerPoint().getMethodName()).isEqualTo("pay");
}
@Test
void mergeRefreshShouldKeepMultiEventFanOutFromSameEntryPoint() {
EntryPoint entryPoint = EntryPoint.builder()
.name("POST /api/order")
.className("com.example.Api")
.methodName("handle")
.build();
CallChain existingPay = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("sendEvent")
.event("PAY")
.build())
.methodChain(List.of("com.example.Api.handle", "pay"))
.build();
CallChain refreshedShip = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("sendEvent")
.event("SHIP")
.build())
.methodChain(List.of("com.example.Api.handle", "ship"))
.build();
CallChain refreshedPay = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("sendEvent")
.event("PAY")
.build())
.methodChain(List.of("com.example.Api.handle"))
.build();
List<CallChain> merged = SemanticCallChainDedup.mergeRefresh(
List.of(existingPay),
List.of(refreshedShip, refreshedPay),
SemanticCallChainDedup.KeepStrategy.SHORTEST);
assertThat(merged).hasSize(2);
assertThat(merged)
.anySatisfy(chain -> {
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("PAY");
assertThat(chain.getMethodChain()).containsExactly("com.example.Api.handle");
});
assertThat(merged)
.anySatisfy(chain -> assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("SHIP"));
}
@Test
void mergeShouldCollapseJsonStubChainWithRefreshedEntryPoint() {
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.web.OrderController")
.methodName("pay")
.event("PAY")
.sourceState("NEW")
.build();
List<MatchedTransition> matched = List.of(MatchedTransition.builder()
.event("PAY")
.sourceState("NEW")
.targetState("PAID")
.build());
CallChain jsonStub = CallChain.builder()
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(matched)
.build();
CallChain refreshed = CallChain.builder()
.entryPoint(EntryPoint.builder()
.className("com.example.web.OrderController")
.methodName("pay")
.build())
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(matched)
.methodChain(List.of("com.example.web.OrderController.pay"))
.build();
List<CallChain> merged = SemanticCallChainDedup.merge(
List.of(jsonStub),
List.of(refreshed),
SemanticCallChainDedup.KeepStrategy.SHORTEST);
assertThat(merged).hasSize(1);
assertThat(merged.get(0).getEntryPoint()).isNotNull();
assertThat(merged.get(0).getMethodChain()).containsExactly("com.example.web.OrderController.pay");
}
@Test
void mergeShouldSemanticDedupAcrossExistingAndRefreshedLists() {
EntryPoint entryPoint = EntryPoint.builder()
.name("POST /api/order/pay")
.className("com.example.Api")
.methodName("pay")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("sendEvent")
.event("PAY")
.build();
List<MatchedTransition> matched = List.of(MatchedTransition.builder()
.event("PAY")
.sourceState("NEW")
.targetState("PAID")
.build());
CallChain existing = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(matched)
.methodChain(List.of("a", "b", "c"))
.build();
CallChain refreshed = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(matched)
.methodChain(List.of("a"))
.build();
List<CallChain> merged = SemanticCallChainDedup.merge(
List.of(existing),
List.of(refreshed),
SemanticCallChainDedup.KeepStrategy.SHORTEST);
assertThat(merged).hasSize(1);
assertThat(merged.get(0).getMethodChain()).containsExactly("a");
}
}

View File

@@ -1028,6 +1028,102 @@ class TransitionLinkerEnricherTest {
assertThat(resultUser.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
}
@Test
void shouldDeduplicateSemanticallyEquivalentChainsKeepingShortestPath() {
Transition payT = new Transition();
payT.setSourceStates(List.of(State.of("NEW", "OrderState.NEW")));
payT.setTargetStates(List.of(State.of("PAID", "OrderState.PAID")));
payT.setEvent(Event.of("OrderEvent.PAY", "com.example.OrderEvent.PAY"));
EntryPoint entryPoint = EntryPoint.builder()
.type(EntryPoint.Type.REST)
.name("POST /api/order/pay")
.className("com.example.Api")
.methodName("pay")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.event("OrderEvent.PAY")
.polymorphicEvents(List.of("OrderEvent.PAY"))
.build();
CallChain longPath = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.methodChain(List.of(
"com.example.Api.pay",
"com.example.OrderService.route",
"com.example.OrderService.delegate",
"com.example.StateMachine.sendEvent"))
.build();
CallChain shortPath = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.methodChain(List.of(
"com.example.Api.pay",
"com.example.StateMachine.sendEvent"))
.build();
AnalysisResult result = AnalysisResult.builder()
.name("OrderStateMachineConfig")
.transitions(List.of(payT))
.metadata(CodebaseMetadata.builder().callChains(List.of(longPath, shortPath)).build())
.build();
enricher.enrich(result, null, null);
assertThat(result.getMetadata().getCallChains()).hasSize(1);
assertThat(result.getMetadata().getCallChains().get(0).getMethodChain())
.containsExactly(
"com.example.Api.pay",
"com.example.StateMachine.sendEvent");
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
}
@Test
void shouldNotStampNonConcreteStringMachineTypeOntoTrigger() {
Transition orderEvent = new Transition();
orderEvent.setSourceStates(List.of(State.of("BUSY", "String.BUSY")));
orderEvent.setTargetStates(List.of(State.of("DONE", "String.DONE")));
orderEvent.setEvent(Event.of("ORDER_EVENT", "String.ORDER_EVENT"));
CallChain chain = CallChain.builder()
.entryPoint(EntryPoint.builder()
.type(EntryPoint.Type.JMS)
.name("JMS: order.queue")
.className("click.kamil.maven.api.JmsOrderListener")
.methodName("onMessage")
.build())
.triggerPoint(TriggerPoint.builder()
.event("ORDER_EVENT")
.polymorphicEvents(List.of("ORDER_EVENT"))
.build())
.methodChain(List.of(
"click.kamil.maven.core.MavenOrderStateMachine.OrderService.onMessage"))
.build();
AnalysisResult result = AnalysisResult.builder()
.name("click.kamil.maven.core.MavenOrderStateMachine")
.eventTypeFqn("String")
.stateTypeFqn("String")
.transitions(List.of(orderEvent))
.metadata(CodebaseMetadata.builder()
.entryPoints(List.of(chain.getEntryPoint()))
.callChains(List.of(chain))
.build())
.build();
enricher.enrich(result, null, null);
assertThat(result.getMetadata().getCallChains()).isNotEmpty();
TriggerPoint linked = result.getMetadata().getCallChains().get(0).getTriggerPoint();
assertThat(linked.getEventTypeFqn())
.as("non-concrete machine String must not be stamped onto the trigger")
.isNull();
assertThat(linked.getStateTypeFqn()).isNull();
assertThat(result.getMetadata().getCallChains().get(0).getLinkResolution())
.isEqualTo(LinkResolution.RESOLVED);
}
@Test
void shouldRejectChainWhenDispatcherDomainConstraintDoesNotMatchMachine() {
Transition payT = new Transition();

View File

@@ -142,6 +142,49 @@ class StrictFqnMatchingEngineTest {
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
}
@Test
void shouldMatchQualifiedStringTriggerToBareConfiguredStringTransition() {
Event smEvent = Event.of("\"INHERITED_SUBMIT\"", "INHERITED_SUBMIT");
TriggerPoint triggerPoint = TriggerPoint.builder()
.event("String.INHERITED_SUBMIT")
.eventTypeFqn("String")
.polymorphicEvents(List.of("String.INHERITED_SUBMIT"))
.build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
}
@Test
void shouldMatchQualifiedStringTriggerToBareWhenEventTypeFqnMissing() {
Event smEvent = Event.of("ORDER_EVENT", "ORDER_EVENT");
TriggerPoint triggerPoint = TriggerPoint.builder()
.event("String.ORDER_EVENT")
.build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
}
@Test
void shouldNotMatchEnumQualifiedTriggerToBareConstant() {
Event smEvent = Event.of("PAY", "PAY");
TriggerPoint triggerPoint = TriggerPoint.builder()
.event("OrderEvent.PAY")
.eventTypeFqn("com.example.OrderEvent")
.build();
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
}
@Test
void shouldNotMatchEnumQualifiedTriggerToBareWhenEventTypeFqnMissing() {
Event smEvent = Event.of("PAY", "PAY");
TriggerPoint triggerPoint = TriggerPoint.builder()
.event("OrderEvent.PAY")
.build();
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
}
@Test
void shouldMatchCanonicalStringLiteralTriggerToConfiguredStringTransition() {
Event smEvent = Event.of("\"AUDIT_EVENT\"", "String.AUDIT_EVENT");

View File

@@ -4,6 +4,7 @@ import click.kamil.springstatemachineexporter.analysis.resolver.ConstraintClause
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
@@ -63,4 +64,30 @@ class CarrierConstraintSupportTest {
"command == ORDER_PAY",
Map.of("commandKey", "order.pay"))).isFalse();
}
@Test
void carrierOnlyConstraintIgnoresMachineDomainNegations() {
String constraint = "\"DOCUMENT\".equalsIgnoreCase(machineType) && \"SUBMIT\".equalsIgnoreCase(event) "
+ "&& !(\"ORDER\".equalsIgnoreCase(machineType))";
assertThat(CarrierConstraintSupport.carrierOnlyConstraint(constraint, Set.of("event")))
.isEqualTo("\"SUBMIT\".equalsIgnoreCase(event)");
}
@Test
void dispatcherDocumentBranchDoesNotContradictEventBinding() {
String constraint = "\"DOCUMENT\".equalsIgnoreCase(machineType) && \"SUBMIT\".equalsIgnoreCase(event) "
+ "&& !(\"ORDER\".equalsIgnoreCase(machineType)) && \"DOCUMENT\".equalsIgnoreCase(machineType)";
assertThat(CarrierConstraintSupport.contradictsCarrierBindings(
constraint, Map.of("event", "SUBMIT", "eventString", "event", "machineType", "DOCUMENT")))
.isFalse();
}
@Test
void dispatcherOrderBranchDoesNotContradictEventBinding() {
String constraint = "\"ORDER\".equalsIgnoreCase(machineType) && \"PAY\".equalsIgnoreCase(event) "
+ "&& \"ORDER\".equalsIgnoreCase(machineType)";
assertThat(CarrierConstraintSupport.contradictsCarrierBindings(
constraint, Map.of("event", "PAY", "eventString", "event", "machineType", "ORDER")))
.isFalse();
}
}

View File

@@ -0,0 +1,152 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.junit.jupiter.api.BeforeEach;
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.Set;
import static org.assertj.core.api.Assertions.assertThat;
class ConstructorInjectionTypeNarrowerTest {
@TempDir
Path tempDir;
private CodebaseContext context;
private InjectionPointAnalyzer injectionAnalyzer;
private ConstructorAnalyzer constructorAnalyzer;
@BeforeEach
void setUp() throws Exception {
Files.writeString(tempDir.resolve("SuperInjection.java"), """
package com.example;
import org.springframework.stereotype.Service;
interface CollaboratorService {
void processEvent(String event);
}
@Service
class MosService implements CollaboratorService {
void processEvent(String event) {}
}
abstract class BaseStateMachineService<T extends CollaboratorService> {
protected final T service;
BaseStateMachineService(T service) {
this.service = service;
}
void handle(String event) {
service.processEvent(event);
}
}
@Service
class MosStateMachineService extends BaseStateMachineService<MosService> {
MosStateMachineService(MosService mosService) {
super(mosService);
}
}
""");
context = new CodebaseContext();
context.setResolveBindings(true);
context.setClasspath(ToolingClasspath.currentJvmJarEntries());
context.scan(Set.of(tempDir), Set.of());
SpringBeanRegistry registry = new SpringBeanRegistry();
for (var cu : context.getCompilationUnits()) {
cu.accept(new SpringContextScanner(registry));
}
injectionAnalyzer = new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
constructorAnalyzer = new ConstructorAnalyzer(context, null);
}
@Test
void shouldIndexSubclassOfGenericBase() {
List<String> impls = context.getImplementations("com.example.BaseStateMachineService");
assertThat(impls).contains("com.example.MosStateMachineService");
}
@Test
void shouldMapSuperConstructorParameterIndexForInheritedField() {
TypeDeclaration subclass = context.getTypeDeclaration("com.example.MosStateMachineService");
TypeDeclaration base = context.getTypeDeclaration("com.example.BaseStateMachineService");
assertThat(context.getSuperclassFqn(subclass)).isEqualTo("com.example.BaseStateMachineService");
MethodDeclaration baseCtor = null;
for (MethodDeclaration md : base.getMethods()) {
if (md.isConstructor()) {
baseCtor = md;
break;
}
}
assertThat(baseCtor).isNotNull();
assertThat(constructorAnalyzer.findAssignedParameterIndexForTest(baseCtor, "service")).isEqualTo(0);
MethodDeclaration ctor = null;
for (MethodDeclaration md : subclass.getMethods()) {
if (md.isConstructor()) {
ctor = md;
break;
}
}
assertThat(ctor).isNotNull();
int idx = constructorAnalyzer.findAssignedParameterIndexForTest(ctor, "service");
assertThat(idx).isEqualTo(0);
}
@Test
void shouldMapInheritedFieldToSubclassConstructorParameter() {
TypeDeclaration subclass = context.getTypeDeclaration("com.example.MosStateMachineService");
String narrowed = constructorAnalyzer.resolveSubclassConstructorParameterFqnForInheritedField(
subclass, "service", injectionAnalyzer);
assertThat(narrowed).isEqualTo("com.example.MosService");
}
@Test
void shouldNarrowFieldBindingInBaseMethod() {
TypeDeclaration base = context.getTypeDeclaration("com.example.BaseStateMachineService");
MethodDeclaration handle = context.findMethodDeclaration(base, "handle", false);
assertThat(handle).isNotNull();
final IVariableBinding[] fieldBinding = {null};
handle.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
if ("processEvent".equals(node.getName().getIdentifier()) && node.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
if (sn.resolveBinding() instanceof IVariableBinding vb) {
fieldBinding[0] = vb;
}
}
return super.visit(node);
}
});
assertThat(fieldBinding[0]).isNotNull();
String narrowed = ConstructorInjectionTypeNarrower.resolveConcreteReceiverTypeFqn(
fieldBinding[0],
base,
context,
injectionAnalyzer,
constructorAnalyzer);
assertThat(narrowed).isEqualTo("com.example.MosService");
}
}

View File

@@ -0,0 +1,239 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
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.analysis.spring.InjectionPointAnalyzer;
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.BeforeEach;
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 java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Regression for generic superclass fields supplied via subclass {@code super(concreteService)}.
*/
class ConstructorSuperInjectionNarrowingTest {
@TempDir
Path tempDir;
private CodebaseContext context;
private JdtCallGraphEngine engine;
@BeforeEach
void setUp() throws Exception {
writeFixture(tempDir);
context = new CodebaseContext();
context.setResolveBindings(true);
context.setClasspath(ToolingClasspath.currentJvmJarEntries());
context.scan(Set.of(tempDir), Set.of());
SpringBeanRegistry registry = new SpringBeanRegistry();
for (var cu : context.getCompilationUnits()) {
cu.accept(new SpringContextScanner(registry));
}
InjectionPointAnalyzer injectionAnalyzer =
new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
engine = new JdtCallGraphEngine(context, injectionAnalyzer);
}
@Test
void shouldNarrowGenericSuperFieldToConcreteSubclassInjection() {
Map<String, List<CallEdge>> graph = engine.buildCallGraph();
List<CallEdge> fromShared = graph.get("com.example.BaseStateMachineService.handle");
assertThat(fromShared)
.as("superclass method must resolve collaborator via subclass ctor injection")
.isNotEmpty();
assertThat(fromShared)
.extracting(CallEdge::getTargetMethod)
.contains("com.example.MosService.processEvent");
assertThat(fromShared)
.extracting(CallEdge::getReceiverTypeFqn)
.containsOnly("com.example.MosService");
assertThat(fromShared)
.extracting(CallEdge::getTargetMethod)
.noneMatch(target -> target.contains("OtherService"));
}
@Test
void shouldProduceSingleChainWithoutSiblingServiceFanOut() {
List<CallChain> chains = engine.findChains(
List.of(EntryPoint.builder()
.type(EntryPoint.Type.CUSTOM)
.className("com.example.MosStateMachineService")
.methodName("dispatch")
.build()),
List.of(TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("sendEvent")
.event("MOS_EVENT")
.build()));
assertThat(chains).isNotEmpty();
assertThat(chains)
.allSatisfy(chain -> assertThat(chain.getMethodChain())
.noneMatch(hop -> hop.contains("OtherService")));
assertThat(chains)
.anyMatch(chain -> chain.getMethodChain().stream()
.anyMatch(hop -> hop.contains("MosService")));
}
private static void writeFixture(Path tempDir) throws Exception {
Files.writeString(tempDir.resolve("SuperInjection.java"), """
package com.example;
import org.springframework.stereotype.Service;
interface CollaboratorService {
void processEvent(String event);
}
@Service
class MosService implements CollaboratorService {
private final StateMachine sm = new StateMachine();
@Override
public void processEvent(String event) {
sm.sendEvent("MOS_EVENT");
}
}
@Service
class OtherService implements CollaboratorService {
private final StateMachine sm = new StateMachine();
@Override
public void processEvent(String event) {
sm.sendEvent("OTHER_EVENT");
}
}
abstract class BaseStateMachineService<T extends CollaboratorService> {
protected final T service;
BaseStateMachineService(T service) {
this.service = service;
}
void handle(String event) {
service.processEvent(event);
}
}
@Service
class MosStateMachineService extends BaseStateMachineService<MosService> {
MosStateMachineService(MosService mosService) {
super(mosService);
}
void dispatch(String event) {
handle(event);
}
}
class StateMachine {
void sendEvent(Object event) {}
}
""");
}
@Test
void shouldPreferBindingOverSameAritySuperOverload() throws Exception {
Path overloadDir = tempDir.resolve("overload");
Files.createDirectories(overloadDir);
Files.writeString(overloadDir.resolve("OverloadSuper.java"), """
package com.example.overload;
import org.springframework.stereotype.Service;
interface CollaboratorService {
void processEvent(String event);
}
@Service
class MosService implements CollaboratorService {
private final StateMachine sm = new StateMachine();
@Override
public void processEvent(String event) {
sm.sendEvent("MOS_EVENT");
}
}
@Service
class OtherService implements CollaboratorService {
private final StateMachine sm = new StateMachine();
@Override
public void processEvent(String event) {
sm.sendEvent("OTHER_EVENT");
}
}
abstract class BaseStateMachineService {
protected final CollaboratorService service;
// Same arity; service is param 0 here.
BaseStateMachineService(CollaboratorService service, String unused) {
this.service = service;
}
// Same arity; service is param 1 here — must not win when binding resolves to the other ctor.
BaseStateMachineService(String unused, CollaboratorService service) {
this.service = service;
}
void handle(String event) {
service.processEvent(event);
}
}
@Service
class MosStateMachineService extends BaseStateMachineService {
MosStateMachineService(MosService mosService, String label) {
super(mosService, label);
}
void dispatch(String event) {
handle(event);
}
}
class StateMachine {
void sendEvent(Object event) {}
}
""");
CodebaseContext overloadContext = new CodebaseContext();
overloadContext.setResolveBindings(true);
overloadContext.setClasspath(ToolingClasspath.currentJvmJarEntries());
overloadContext.scan(Set.of(overloadDir), Set.of());
SpringBeanRegistry registry = new SpringBeanRegistry();
for (var cu : overloadContext.getCompilationUnits()) {
cu.accept(new SpringContextScanner(registry));
}
InjectionPointAnalyzer injectionAnalyzer =
new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
JdtCallGraphEngine overloadEngine = new JdtCallGraphEngine(overloadContext, injectionAnalyzer);
Map<String, List<CallEdge>> graph = overloadEngine.buildCallGraph();
List<CallEdge> fromShared = graph.get("com.example.overload.BaseStateMachineService.handle");
assertThat(fromShared)
.extracting(CallEdge::getReceiverTypeFqn)
.containsOnly("com.example.overload.MosService");
assertThat(fromShared)
.extracting(CallEdge::getTargetMethod)
.noneMatch(target -> target.contains("OtherService"));
}
}

View File

@@ -0,0 +1,78 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.enricher.SemanticCallChainDedup;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.model.Event;
import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Transition;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class EnrichmentServiceSemanticMergeTest {
@Test
void refreshMergeShouldUseSemanticDedupNotEntryPointOnly() {
EntryPoint entryPoint = EntryPoint.builder()
.name("POST /api/order/pay")
.className("com.example.Api")
.methodName("pay")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.event("PAY")
.build();
List<MatchedTransition> matched = List.of(MatchedTransition.builder()
.event("PAY")
.sourceState("NEW")
.targetState("PAID")
.build());
CallChain existing = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(matched)
.methodChain(List.of("a", "b", "c"))
.build();
CallChain refreshed = CallChain.builder()
.entryPoint(entryPoint)
.triggerPoint(trigger)
.linkResolution(LinkResolution.RESOLVED)
.matchedTransitions(matched)
.methodChain(List.of("a"))
.build();
List<CallChain> merged = SemanticCallChainDedup.mergeRefresh(
List.of(existing),
List.of(refreshed),
SemanticCallChainDedup.KeepStrategy.SHORTEST);
assertThat(merged).hasSize(1);
assertThat(merged.get(0).getMethodChain()).containsExactly("a");
Transition payT = new Transition();
payT.setSourceStates(List.of(State.of("NEW", "NEW")));
payT.setTargetStates(List.of(State.of("PAID", "PAID")));
payT.setEvent(Event.of("PAY", "PAY"));
AnalysisResult result = AnalysisResult.builder()
.name("OrderStateMachineConfig")
.transitions(List.of(payT))
.metadata(CodebaseMetadata.builder().callChains(merged).build())
.build();
new EnrichmentService(List.of()).relinkAfterPropertyResolution(result, null, null);
assertThat(result.getMetadata().getCallChains()).hasSize(1);
assertThat(result.getMetadata().getCallChains().get(0).getMethodChain()).containsExactly("a");
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
}
}

View File

@@ -0,0 +1,82 @@
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.List;
import java.util.stream.StreamSupport;
import static org.assertj.core.api.Assertions.assertThat;
/**
* String-typed machines with an abstract ConfigurerAdapter base must keep package-proven
* JMS/REST chains. Linker must not stamp non-concrete {@code String} type FQNs onto triggers
* and then fail-closed as ambiguous shared generics.
*/
class MavenOrderStringMachineLinkageRegressionTest {
@Test
void coreModuleExportRetainsJmsAndRestResolvedChains(@TempDir Path tempDir) throws Exception {
Path coreModule = findProjectRoot().resolve("state_machines/maven_multi_module/core-module");
ExportService exportService = new ExportService(List.of(new JsonExporter()));
exportService.runExporter(coreModule, tempDir, List.of("json"), true, List.of(), null, null,
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
JsonNode json = readMachineJson(tempDir, "MavenOrderStateMachine", new ObjectMapper());
JsonNode chains = json.path("metadata").path("callChains");
JsonNode entryPoints = json.path("metadata").path("entryPoints");
assertThat(chains.isArray()).isTrue();
assertThat(chains.size()).isGreaterThanOrEqualTo(2);
assertThat(entryPoints.isArray()).isTrue();
assertThat(entryPoints.size()).isGreaterThanOrEqualTo(2);
JsonNode jms = findChainByEndpoint(json, "JMS: order.queue");
assertThat(jms.path("linkResolution").asText()).isEqualTo("RESOLVED");
assertThat(jms.path("triggerPoint").path("event").asText()).contains("ORDER_EVENT");
assertThat(jms.path("matchedTransitions")).isNotEmpty();
JsonNode rest = findChainByEndpoint(json, "POST /maven/orders/submit");
assertThat(rest.path("linkResolution").asText()).isEqualTo("RESOLVED");
assertThat(rest.path("triggerPoint").path("event").asText()).contains("SUBMIT");
assertThat(rest.path("matchedTransitions")).isNotEmpty();
assertThat(StreamSupport.stream(entryPoints.spliterator(), false)
.anyMatch(ep -> "JMS".equals(ep.path("type").asText()))).isTrue();
}
private static JsonNode findChainByEndpoint(JsonNode machineJson, String endpointName) {
return StreamSupport.stream(machineJson.path("metadata").path("callChains").spliterator(), false)
.filter(chain -> endpointName.equals(chain.path("entryPoint").path("name").asText()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("chain not found for " + endpointName));
}
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));
}
return mapper.readTree(machineDir.resolve(machineDir.getFileName() + ".json").toFile());
}
private static Path findProjectRoot() {
Path current = Path.of(".").toAbsolutePath();
while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
current = current.getParent();
}
return current;
}
}

View File

@@ -80,7 +80,7 @@ class PathResolutionOptimizationTest {
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getPathBindings())
.containsEntry("command", "DomainCommand.ORDER_PAY");
.containsEntry("command", "com.example.DomainCommand.ORDER_PAY");
assertThat(chains.get(0).getMethodChain()).contains("com.example.CentralDispatcher.orderPay");
}

View File

@@ -358,15 +358,15 @@
},
"methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.placeOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.place" ],
"triggerPoint" : {
"event" : "java.lang.String.PLACE_ORDER",
"event" : "String.PLACE_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
"methodName" : "place",
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/service/OrderServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "java.lang.String.NEW",
"sourceState" : "String.NEW",
"lineNumber" : 16,
"polymorphicEvents" : [ "java.lang.String.PLACE_ORDER" ],
"polymorphicEvents" : [ "String.PLACE_ORDER" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
@@ -397,15 +397,15 @@
},
"methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.cancelOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.cancel" ],
"triggerPoint" : {
"event" : "java.lang.String.CANCEL_ORDER",
"event" : "String.CANCEL_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
"methodName" : "cancel",
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/service/OrderServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "java.lang.String.PAID",
"sourceState" : "String.PAID",
"lineNumber" : 21,
"polymorphicEvents" : [ "java.lang.String.CANCEL_ORDER" ],
"polymorphicEvents" : [ "String.CANCEL_ORDER" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
@@ -431,7 +431,7 @@
},
"methodChain" : [ "click.kamil.examples.enterprise.api.SecurityInterceptor.preHandle" ],
"triggerPoint" : {
"event" : "java.lang.String.AUDIT_EVENT",
"event" : "String.AUDIT_EVENT",
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
"methodName" : "preHandle",
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/api/SecurityInterceptor.java",
@@ -466,15 +466,15 @@
},
"methodChain" : [ "click.kamil.examples.enterprise.api.ReactivePaymentController.pay", "click.kamil.examples.enterprise.service.ReactivePaymentService.processPayment" ],
"triggerPoint" : {
"event" : "java.lang.String.PAY_ORDER",
"event" : "String.PAY_ORDER",
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
"methodName" : "processPayment",
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/service/ReactivePaymentService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "java.lang.String.PENDING_PAYMENT",
"sourceState" : "String.PENDING_PAYMENT",
"lineNumber" : 18,
"polymorphicEvents" : [ "java.lang.String.PAY_ORDER" ],
"polymorphicEvents" : [ "String.PAY_ORDER" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
@@ -505,13 +505,13 @@
},
"methodChain" : [ "click.kamil.examples.enterprise.messaging.ShippingJmsListener.onShippingReady" ],
"triggerPoint" : {
"event" : "java.lang.String.SHIP_ORDER",
"event" : "String.SHIP_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
"methodName" : "onShippingReady",
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ShippingJmsListener.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "java.lang.String.PAID",
"sourceState" : "String.PAID",
"lineNumber" : 17,
"polymorphicEvents" : null,
"external" : false,
@@ -544,13 +544,13 @@
},
"methodChain" : [ "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener.onReturn" ],
"triggerPoint" : {
"event" : "java.lang.String.RETURN_ORDER",
"event" : "String.RETURN_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
"methodName" : "onReturn",
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ReturnsRabbitListener.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "java.lang.String.DELIVERED",
"sourceState" : "String.DELIVERED",
"lineNumber" : 17,
"polymorphicEvents" : null,
"external" : false,

View File

@@ -189,15 +189,15 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.reactiveOrder", "click.kamil.examples.statemachine.extended.service.ReactiveOrderService.processReactive" ],
"triggerPoint" : {
"event" : "java.lang.String.REACTIVE_EVENT",
"event" : "String.REACTIVE_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
"methodName" : "processReactive",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ReactiveOrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "java.lang.String.START",
"sourceState" : "String.START",
"lineNumber" : 18,
"polymorphicEvents" : [ "java.lang.String.REACTIVE_EVENT" ],
"polymorphicEvents" : [ "String.REACTIVE_EVENT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false

View File

@@ -189,15 +189,15 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.reactiveOrder", "click.kamil.examples.statemachine.extended.service.ReactiveOrderService.processReactive" ],
"triggerPoint" : {
"event" : "java.lang.String.REACTIVE_EVENT",
"event" : "String.REACTIVE_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
"methodName" : "processReactive",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ReactiveOrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "java.lang.String.START",
"sourceState" : "String.START",
"lineNumber" : 18,
"polymorphicEvents" : [ "java.lang.String.REACTIVE_EVENT" ],
"polymorphicEvents" : [ "String.REACTIVE_EVENT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false

View File

@@ -73,15 +73,15 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.inheritance.api.OrderControllerImpl.submitOrder", "click.kamil.examples.statemachine.inheritance.service.ProcessingServiceImpl.doProcess" ],
"triggerPoint" : {
"event" : "java.lang.String.INHERITED_SUBMIT",
"event" : "String.INHERITED_SUBMIT",
"className" : "click.kamil.examples.statemachine.inheritance.service.ProcessingServiceImpl",
"methodName" : "doProcess",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/inheritance/service/ProcessingServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "java.lang.String.START",
"sourceState" : "String.START",
"lineNumber" : 15,
"polymorphicEvents" : [ "java.lang.String.INHERITED_SUBMIT" ],
"polymorphicEvents" : [ "String.INHERITED_SUBMIT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false

View File

@@ -61,6 +61,21 @@
"metadata" : {
"triggers" : [ ],
"entryPoints" : [ {
"type" : "REST",
"name" : "POST /maven/orders/submit",
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderController",
"methodName" : "submitOrder",
"sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java",
"metadata" : {
"path" : "/maven/orders/submit",
"verb" : "POST"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ "RequestBody" ]
} ]
}, {
"type" : "JMS",
"name" : "JMS: order.queue",
"className" : "click.kamil.maven.api.JmsOrderListener",
@@ -77,6 +92,45 @@
} ]
} ],
"callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /maven/orders/submit",
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderController",
"methodName" : "submitOrder",
"sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java",
"metadata" : {
"path" : "/maven/orders/submit",
"verb" : "POST"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ "RequestBody" ]
} ]
},
"methodChain" : [ "click.kamil.maven.core.MavenOrderStateMachine.OrderController.submitOrder" ],
"triggerPoint" : {
"event" : "String.SUBMIT",
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderController",
"methodName" : "submitOrder",
"sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "String.INIT",
"lineNumber" : 40,
"polymorphicEvents" : null,
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "String.INIT",
"targetState" : "String.BUSY",
"event" : "String.SUBMIT"
} ],
"linkResolution" : "RESOLVED"
}, {
"entryPoint" : {
"type" : "JMS",
"name" : "JMS: order.queue",
@@ -93,17 +147,17 @@
"annotations" : [ ]
} ]
},
"methodChain" : [ "click.kamil.maven.api.JmsOrderListener.onMessage", "click.kamil.maven.core.MavenOrderStateMachine.OrderService.onMessage" ],
"methodChain" : [ "click.kamil.maven.core.MavenOrderStateMachine.OrderService.onMessage" ],
"triggerPoint" : {
"event" : "java.lang.String.ORDER_EVENT",
"event" : "String.ORDER_EVENT",
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderService",
"methodName" : "onMessage",
"sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "java.lang.String.BUSY",
"sourceState" : "String.BUSY",
"lineNumber" : 52,
"polymorphicEvents" : [ "java.lang.String.ORDER_EVENT" ],
"polymorphicEvents" : null,
"external" : false,
"constraint" : null,
"ambiguous" : false

View File

@@ -108,13 +108,13 @@
},
"methodChain" : [ "click.kamil.multi.core.OrderController.submitOrder" ],
"triggerPoint" : {
"event" : "java.lang.String.SUBMIT",
"event" : "String.SUBMIT",
"className" : "click.kamil.multi.core.OrderController",
"methodName" : "submitOrder",
"sourceFile" : "core-module/src/main/java/click/kamil/multi/core/OrderController.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "java.lang.String.NEW",
"sourceState" : "String.NEW",
"lineNumber" : 18,
"polymorphicEvents" : null,
"external" : false,

View File

@@ -367,15 +367,15 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "java.lang.String.NAMED_EVENT",
"event" : "String.NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : "paymentStateMachine",
"sourceState" : "java.lang.String.NEW",
"sourceState" : "String.NEW",
"lineNumber" : 17,
"polymorphicEvents" : [ "java.lang.String.NAMED_EVENT" ],
"polymorphicEvents" : [ "String.NAMED_EVENT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
@@ -402,15 +402,15 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "java.lang.String.PRIMARY_EVENT",
"event" : "String.PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : "paymentStateMachine",
"sourceState" : "java.lang.String.NEW",
"sourceState" : "String.NEW",
"lineNumber" : 19,
"polymorphicEvents" : [ "java.lang.String.PRIMARY_EVENT" ],
"polymorphicEvents" : [ "String.PRIMARY_EVENT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
@@ -437,15 +437,15 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "java.lang.String.NAMED_EVENT",
"event" : "String.NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : "paymentStateMachine",
"sourceState" : "java.lang.String.NEW",
"sourceState" : "String.NEW",
"lineNumber" : 17,
"polymorphicEvents" : [ "java.lang.String.NAMED_EVENT" ],
"polymorphicEvents" : [ "String.NAMED_EVENT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
@@ -472,15 +472,15 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "java.lang.String.QUALIFIER_EVENT",
"event" : "String.QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
"sourceModule" : null,
"stateMachineId" : "paymentStateMachine",
"sourceState" : "java.lang.String.NEW",
"sourceState" : "String.NEW",
"lineNumber" : 17,
"polymorphicEvents" : [ "java.lang.String.QUALIFIER_EVENT" ],
"polymorphicEvents" : [ "String.QUALIFIER_EVENT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
@@ -507,15 +507,15 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "java.lang.String.PRIMARY_EVENT",
"event" : "String.PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : "paymentStateMachine",
"sourceState" : "java.lang.String.NEW",
"sourceState" : "String.NEW",
"lineNumber" : 19,
"polymorphicEvents" : [ "java.lang.String.PRIMARY_EVENT" ],
"polymorphicEvents" : [ "String.PRIMARY_EVENT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
@@ -542,15 +542,15 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "java.lang.String.QUALIFIER_EVENT",
"event" : "String.QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
"sourceModule" : null,
"stateMachineId" : "paymentStateMachine",
"sourceState" : "java.lang.String.NEW",
"sourceState" : "String.NEW",
"lineNumber" : 17,
"polymorphicEvents" : [ "java.lang.String.QUALIFIER_EVENT" ],
"polymorphicEvents" : [ "String.QUALIFIER_EVENT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
@@ -577,15 +577,15 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "java.lang.String.NAMED_EVENT",
"event" : "String.NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : "paymentStateMachine",
"sourceState" : "java.lang.String.NEW",
"sourceState" : "String.NEW",
"lineNumber" : 17,
"polymorphicEvents" : [ "java.lang.String.NAMED_EVENT" ],
"polymorphicEvents" : [ "String.NAMED_EVENT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
@@ -616,15 +616,15 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.processBaseEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.processPayment" ],
"triggerPoint" : {
"event" : "java.lang.String.AUTHORIZE",
"event" : "String.AUTHORIZE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "processPayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : "paymentStateMachine",
"sourceState" : "java.lang.String.NEW",
"sourceState" : "String.NEW",
"lineNumber" : 22,
"polymorphicEvents" : [ "java.lang.String.AUTHORIZE" ],
"polymorphicEvents" : [ "String.AUTHORIZE" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
@@ -655,15 +655,15 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
"triggerPoint" : {
"event" : "java.lang.String.CAPTURE",
"event" : "String.CAPTURE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : "paymentStateMachine",
"sourceState" : "java.lang.String.AUTHORIZED",
"sourceState" : "String.AUTHORIZED",
"lineNumber" : 35,
"polymorphicEvents" : [ "java.lang.String.CAPTURE" ],
"polymorphicEvents" : [ "String.CAPTURE" ],
"external" : false,
"constraint" : null,
"ambiguous" : false