A
This commit is contained in:
@@ -88,10 +88,10 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only unbound *event* path templates (e.g. /{event}) stay unresolved-external.
|
||||
// Unbound event templates (REST /{event}) and unbound messaging payloads stay unresolved.
|
||||
// Resource placeholders like /{id}/pay must still link when the event is known.
|
||||
if (tp.isExternal()
|
||||
&& isUnresolvedGenericRestEndpoint(chain.getEntryPoint())
|
||||
&& isUnresolvedExternalEndpoint(chain, tp)
|
||||
&& !hasConcreteEventEvidence(tp)) {
|
||||
updatedChains.add(chain.toBuilder()
|
||||
.triggerPoint(tp)
|
||||
@@ -103,7 +103,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
|
||||
// Clear spurious external flags (unrelated PathVariable/RequestParam) once we can link.
|
||||
if (tp.isExternal()
|
||||
&& (!isUnresolvedGenericRestEndpoint(chain.getEntryPoint()) || hasConcreteEventEvidence(tp))) {
|
||||
&& (!isUnresolvedExternalEndpoint(chain, tp) || hasConcreteEventEvidence(tp))) {
|
||||
tp = tp.toBuilder().external(false).build();
|
||||
}
|
||||
|
||||
@@ -288,7 +288,22 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
if (constraint == null || constraint.isBlank()) {
|
||||
return constraint;
|
||||
}
|
||||
String stripped = constraint.replaceAll("\"[^\"]+\"\\.equalsIgnoreCase\\(event\\)", "true");
|
||||
// Event/payload binding clauses prove which event was selected — not which machine.
|
||||
// Keep machineType/domain comparisons for isCompatibleWithMachineDomain.
|
||||
String eventLike =
|
||||
"(?i)(event|e|message|msg|payload|body|command|commandKey|commandkey|action|text)";
|
||||
String stripped = constraint.replaceAll(
|
||||
"\"[^\"]+\"\\s*\\.\\s*equalsIgnoreCase\\s*\\(\\s*" + eventLike + "\\s*\\)",
|
||||
"true");
|
||||
stripped = stripped.replaceAll(
|
||||
eventLike + "\\s*\\.\\s*equalsIgnoreCase\\s*\\(\\s*\"[^\"]+\"\\s*\\)",
|
||||
"true");
|
||||
stripped = stripped.replaceAll(
|
||||
"\"[^\"]+\"\\s*\\.\\s*equals\\s*\\(\\s*" + eventLike + "\\s*\\)",
|
||||
"true");
|
||||
stripped = stripped.replaceAll(
|
||||
eventLike + "\\s*\\.\\s*equals\\s*\\(\\s*\"[^\"]+\"\\s*\\)",
|
||||
"true");
|
||||
stripped = stripped.replaceAll("&&\\s*&&+", "&&");
|
||||
stripped = stripped.replaceAll("^\\s*&&\\s*", "");
|
||||
stripped = stripped.replaceAll("\\s*&&\\s*$", "");
|
||||
@@ -347,6 +362,36 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
* True when the entry path still has an unbound event-carrying placeholder
|
||||
* ({@code {event}}, {@code {commandKey}}, …). Resource ids like {@code {id}} do not count.
|
||||
*/
|
||||
private static boolean isUnresolvedExternalEndpoint(CallChain chain, TriggerPoint trigger) {
|
||||
if (chain == null) {
|
||||
return false;
|
||||
}
|
||||
if (isUnresolvedGenericRestEndpoint(chain.getEntryPoint())) {
|
||||
return true;
|
||||
}
|
||||
return isUnresolvedMessagingPayloadEndpoint(chain, trigger);
|
||||
}
|
||||
|
||||
private static boolean isUnresolvedMessagingPayloadEndpoint(CallChain chain, TriggerPoint trigger) {
|
||||
EntryPoint entryPoint = chain.getEntryPoint();
|
||||
if (entryPoint == null || entryPoint.getType() == null || trigger == null) {
|
||||
return false;
|
||||
}
|
||||
boolean messaging = switch (entryPoint.getType()) {
|
||||
case JMS, KAFKA, RABBIT, SQS, SNS -> true;
|
||||
default -> false;
|
||||
};
|
||||
if (!messaging) {
|
||||
return false;
|
||||
}
|
||||
if (!MachineEnumCanonicalizer.isDynamicTriggerExpression(trigger.getEvent())) {
|
||||
return false;
|
||||
}
|
||||
// Expander variants carry concrete payload bindings (same channel as REST pathBindings).
|
||||
Map<String, String> bindings = chain.getPathBindings();
|
||||
return bindings == null || bindings.isEmpty();
|
||||
}
|
||||
|
||||
private static boolean isUnresolvedGenericRestEndpoint(EntryPoint entryPoint) {
|
||||
if (entryPoint == null) {
|
||||
return false;
|
||||
|
||||
@@ -2262,7 +2262,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
|| typeName.endsWith("PathParam") || typeName.endsWith("QueryParam")) {
|
||||
return true;
|
||||
}
|
||||
if (typeName.endsWith("RequestBody")) {
|
||||
if (typeName.endsWith("RequestBody") || typeName.endsWith("Payload")) {
|
||||
ITypeBinding typeBinding = svd.getType().resolveBinding();
|
||||
if (typeBinding != null && typeBinding.isEnum()) {
|
||||
return false;
|
||||
|
||||
@@ -12,8 +12,9 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Expands REST entry points whose {@code @PathVariable} parameters flow into string-keyed
|
||||
* switch mappers (e.g. {@code StringCommandMapper.fromString}) into concrete binding variants.
|
||||
* Expands entry points whose string parameters flow into switch/map/`valueOf` mappers
|
||||
* into concrete binding variants. Covers REST {@code @PathVariable}/{@code @RequestParam}
|
||||
* and messaging {@code @Payload} / message-body string parameters (JMS, Kafka, Rabbit, …).
|
||||
*/
|
||||
public final class EntryPointBindingExpander {
|
||||
|
||||
@@ -21,7 +22,7 @@ public final class EntryPointBindingExpander {
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands an entry point into concrete binding variants from path/query parameters:
|
||||
* Expands an entry point into concrete binding variants from expandable parameters:
|
||||
* string switch/if arms and boolean ternary conditions provable in source.
|
||||
*/
|
||||
public static List<Map<String, String>> expandEntryPointBindings(
|
||||
@@ -57,10 +58,12 @@ public final class EntryPointBindingExpander {
|
||||
List<Map<String, String>> variants = new ArrayList<>();
|
||||
variants.add(new LinkedHashMap<>());
|
||||
for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
|
||||
if (!isPathOrQueryVariable(parameter)) {
|
||||
if (!isExpandableBindingParameter(entryPoint, parameter)) {
|
||||
continue;
|
||||
}
|
||||
if (!isRequestParam(parameter) && isPlaceholderResolvedInPath(entryPoint, parameter.getName())) {
|
||||
if (!isRequestParam(parameter)
|
||||
&& !isMessagingPayloadParameter(entryPoint, parameter)
|
||||
&& isPlaceholderResolvedInPath(entryPoint, parameter.getName())) {
|
||||
continue;
|
||||
}
|
||||
List<Map<String, String>> expanded = new ArrayList<>();
|
||||
@@ -120,6 +123,7 @@ public final class EntryPointBindingExpander {
|
||||
metadata.put("path", path.replace(placeholder, binding.getValue()));
|
||||
}
|
||||
} else if (isRequestParamParameter(entryPoint, binding.getKey())
|
||||
|| isMessagingPayloadBinding(entryPoint, binding.getKey())
|
||||
|| isBooleanBindingValue(binding.getValue())) {
|
||||
if (querySuffix.isEmpty()) {
|
||||
querySuffix.append('?');
|
||||
@@ -275,19 +279,90 @@ public final class EntryPointBindingExpander {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isPathOrQueryVariable(EntryPoint.Parameter parameter) {
|
||||
if (parameter.getAnnotations() == null) {
|
||||
private static boolean isMessagingPayloadBinding(EntryPoint entryPoint, String paramName) {
|
||||
if (entryPoint == null || entryPoint.getParameters() == null || paramName == null) {
|
||||
return false;
|
||||
}
|
||||
return parameter.getAnnotations().stream()
|
||||
.anyMatch(a -> "PathVariable".equals(a) || "RequestParam".equals(a));
|
||||
for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
|
||||
if (paramName.equals(parameter.getName()) && isMessagingPayloadParameter(entryPoint, parameter)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isExpandableBindingParameter(EntryPoint entryPoint, EntryPoint.Parameter parameter) {
|
||||
return isPathOrQueryVariable(parameter) || isMessagingPayloadParameter(entryPoint, parameter);
|
||||
}
|
||||
|
||||
private static boolean isPathOrQueryVariable(EntryPoint.Parameter parameter) {
|
||||
return hasSimpleAnnotation(parameter, "PathVariable") || hasSimpleAnnotation(parameter, "RequestParam");
|
||||
}
|
||||
|
||||
private static boolean isRequestParam(EntryPoint.Parameter parameter) {
|
||||
if (parameter.getAnnotations() == null) {
|
||||
return hasSimpleAnnotation(parameter, "RequestParam");
|
||||
}
|
||||
|
||||
/**
|
||||
* Messaging listeners carry the event key in {@code @Payload} or an unannotated String body
|
||||
* ({@code message}, {@code payload}, {@code event}, …) — same expansion surface as REST path vars.
|
||||
*/
|
||||
private static boolean isMessagingPayloadParameter(EntryPoint entryPoint, EntryPoint.Parameter parameter) {
|
||||
if (entryPoint == null || parameter == null || !isMessagingEntryPoint(entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
return parameter.getAnnotations().stream().anyMatch("RequestParam"::equals);
|
||||
if (hasSimpleAnnotation(parameter, "Payload")) {
|
||||
return true;
|
||||
}
|
||||
boolean unannotated = parameter.getAnnotations() == null || parameter.getAnnotations().isEmpty();
|
||||
if (!unannotated) {
|
||||
return false;
|
||||
}
|
||||
String type = parameter.getType();
|
||||
if (type == null) {
|
||||
return false;
|
||||
}
|
||||
String simpleType = type.contains(".") ? type.substring(type.lastIndexOf('.') + 1) : type;
|
||||
if (!"String".equals(simpleType)) {
|
||||
return false;
|
||||
}
|
||||
return isMessageOrEventParamName(parameter.getName());
|
||||
}
|
||||
|
||||
private static boolean isMessagingEntryPoint(EntryPoint entryPoint) {
|
||||
if (entryPoint.getType() == null) {
|
||||
return false;
|
||||
}
|
||||
return switch (entryPoint.getType()) {
|
||||
case JMS, KAFKA, RABBIT, SQS, SNS -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean isMessageOrEventParamName(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String lower = name.toLowerCase();
|
||||
return lower.equals("message")
|
||||
|| lower.equals("msg")
|
||||
|| lower.equals("payload")
|
||||
|| lower.equals("body")
|
||||
|| lower.equals("event")
|
||||
|| lower.equals("e")
|
||||
|| lower.endsWith("event")
|
||||
|| lower.equals("command")
|
||||
|| lower.equals("commandkey")
|
||||
|| lower.equals("action")
|
||||
|| lower.equals("text");
|
||||
}
|
||||
|
||||
private static boolean hasSimpleAnnotation(EntryPoint.Parameter parameter, String simpleName) {
|
||||
if (parameter.getAnnotations() == null || simpleName == null) {
|
||||
return false;
|
||||
}
|
||||
return parameter.getAnnotations().stream().anyMatch(a ->
|
||||
simpleName.equals(a) || (a != null && a.endsWith("." + simpleName)));
|
||||
}
|
||||
|
||||
private static boolean isRequestParamParameter(EntryPoint entryPoint, String paramName) {
|
||||
|
||||
@@ -9,7 +9,8 @@ import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Single source-derived policy for marking REST triggers as external vs internal.
|
||||
* Single source-derived policy for marking entry-point triggers as external vs internal
|
||||
* (REST path/query/body and messaging payload parameters).
|
||||
*/
|
||||
public final class ExternalTriggerPolicy {
|
||||
|
||||
@@ -79,13 +80,34 @@ public final class ExternalTriggerPolicy {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (entryPoint != null && isMessagingEntryPoint(entryPoint)) {
|
||||
String paramName = resolveMessagingEventParameterName(entryPoint, resolvedEventParamName);
|
||||
if (paramName != null) {
|
||||
RestParamBinding binding = findRestParameterBinding(entryPoint, paramName, context);
|
||||
if (binding != null && binding.payloadEnum()) {
|
||||
return false;
|
||||
}
|
||||
RestParamBinding astBinding = entryPoint.getClassName() != null && entryPoint.getMethodName() != null
|
||||
? findMethodParameterBinding(
|
||||
entryPoint.getClassName() + "." + entryPoint.getMethodName(), paramName, context)
|
||||
: null;
|
||||
if (astBinding != null && astBinding.payloadEnum()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (MachineEnumCanonicalizer.isDynamicTriggerExpression(trigger.getEvent())
|
||||
&& !hasConcreteSinglePoly(trigger)) {
|
||||
// Unbound messaging payload feeding valueOf/switch — treat like unbound REST event param.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (entryMethodFqn != null && resolvedEventParamName != null) {
|
||||
RestParamBinding binding = findMethodParameterBinding(entryMethodFqn, resolvedEventParamName, context);
|
||||
if (binding != null) {
|
||||
if (binding.pathOrQueryVariable() && !binding.enumType()) {
|
||||
return true;
|
||||
}
|
||||
if (binding.requestBodyEnum()) {
|
||||
if (binding.requestBodyEnum() || binding.payloadEnum()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -103,6 +125,54 @@ public final class ExternalTriggerPolicy {
|
||||
return trigger.isExternal();
|
||||
}
|
||||
|
||||
private static boolean isMessagingEntryPoint(EntryPoint entryPoint) {
|
||||
if (entryPoint == null || entryPoint.getType() == null) {
|
||||
return false;
|
||||
}
|
||||
return switch (entryPoint.getType()) {
|
||||
case JMS, KAFKA, RABBIT, SQS, SNS -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean hasConcreteSinglePoly(TriggerPoint trigger) {
|
||||
if (trigger.getPolymorphicEvents() == null || trigger.getPolymorphicEvents().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(trigger.getPolymorphicEvents())
|
||||
&& trigger.getPolymorphicEvents().size() == 1;
|
||||
}
|
||||
|
||||
private static String resolveMessagingEventParameterName(EntryPoint entryPoint, String resolvedEventParamName) {
|
||||
if (resolvedEventParamName != null && !resolvedEventParamName.isBlank()
|
||||
&& looksLikeParameterName(resolvedEventParamName)) {
|
||||
return resolvedEventParamName;
|
||||
}
|
||||
if (entryPoint.getParameters() == null) {
|
||||
return null;
|
||||
}
|
||||
for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
|
||||
if (parameter.getAnnotations() != null
|
||||
&& parameter.getAnnotations().stream().anyMatch(a ->
|
||||
"Payload".equals(a) || (a != null && a.endsWith(".Payload")))) {
|
||||
return parameter.getName();
|
||||
}
|
||||
}
|
||||
for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
|
||||
if ((parameter.getAnnotations() == null || parameter.getAnnotations().isEmpty())
|
||||
&& isEventLikeParameterName(parameter.getName())) {
|
||||
return parameter.getName();
|
||||
}
|
||||
String lower = parameter.getName() == null ? "" : parameter.getName().toLowerCase();
|
||||
if ((parameter.getAnnotations() == null || parameter.getAnnotations().isEmpty())
|
||||
&& (lower.equals("message") || lower.equals("msg") || lower.equals("payload")
|
||||
|| lower.equals("body") || lower.equals("text"))) {
|
||||
return parameter.getName();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean entryPointHasUnboundPlaceholders(EntryPoint entryPoint) {
|
||||
if (entryPoint == null) {
|
||||
return false;
|
||||
@@ -276,13 +346,13 @@ public final class ExternalTriggerPolicy {
|
||||
continue;
|
||||
}
|
||||
if (isEnumRecordComponent(bodyBinding, fieldName)) {
|
||||
return new RestParamBinding(false, false, true, true);
|
||||
return new RestParamBinding(false, false, true, false, true);
|
||||
}
|
||||
if (isEnumDtoField(bodyBinding, fieldName, context)) {
|
||||
return new RestParamBinding(false, false, true, true);
|
||||
return new RestParamBinding(false, false, true, false, true);
|
||||
}
|
||||
if (containsNestedEnumField(bodyBinding, fieldName, context)) {
|
||||
return new RestParamBinding(false, false, true, true);
|
||||
return new RestParamBinding(false, false, true, false, true);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -371,7 +441,12 @@ public final class ExternalTriggerPolicy {
|
||||
return false;
|
||||
}
|
||||
|
||||
private record RestParamBinding(boolean pathVariable, boolean requestParam, boolean requestBody, boolean enumType) {
|
||||
private record RestParamBinding(
|
||||
boolean pathVariable,
|
||||
boolean requestParam,
|
||||
boolean requestBody,
|
||||
boolean payload,
|
||||
boolean enumType) {
|
||||
boolean pathOrQueryVariable() {
|
||||
return pathVariable || requestParam;
|
||||
}
|
||||
@@ -380,25 +455,32 @@ public final class ExternalTriggerPolicy {
|
||||
return requestBody && enumType;
|
||||
}
|
||||
|
||||
boolean payloadEnum() {
|
||||
return payload && enumType;
|
||||
}
|
||||
|
||||
static RestParamBinding fromEntryPointParameter(EntryPoint.Parameter parameter) {
|
||||
boolean pathVariable = hasAnnotation(parameter.getAnnotations(), "PathVariable");
|
||||
boolean requestParam = hasAnnotation(parameter.getAnnotations(), "RequestParam");
|
||||
boolean requestBody = hasAnnotation(parameter.getAnnotations(), "RequestBody");
|
||||
boolean payload = hasAnnotation(parameter.getAnnotations(), "Payload");
|
||||
boolean enumType = isEnumTypeName(parameter.getType());
|
||||
return new RestParamBinding(pathVariable, requestParam, requestBody, enumType);
|
||||
return new RestParamBinding(pathVariable, requestParam, requestBody, payload, enumType);
|
||||
}
|
||||
|
||||
static RestParamBinding fromAstParameter(SingleVariableDeclaration param) {
|
||||
boolean pathVariable = hasParameterAstAnnotation(param, "PathVariable");
|
||||
boolean requestParam = hasParameterAstAnnotation(param, "RequestParam");
|
||||
boolean requestBody = hasParameterAstAnnotation(param, "RequestBody");
|
||||
boolean payload = hasParameterAstAnnotation(param, "Payload");
|
||||
boolean enumType = param.getType() != null && param.getType().resolveBinding() != null
|
||||
&& param.getType().resolveBinding().isEnum();
|
||||
return new RestParamBinding(pathVariable, requestParam, requestBody, enumType);
|
||||
return new RestParamBinding(pathVariable, requestParam, requestBody, payload, enumType);
|
||||
}
|
||||
|
||||
private static boolean hasAnnotation(java.util.List<String> annotations, String name) {
|
||||
return annotations != null && annotations.stream().anyMatch(name::equals);
|
||||
return annotations != null && annotations.stream().anyMatch(a ->
|
||||
name.equals(a) || (a != null && a.endsWith("." + name)));
|
||||
}
|
||||
|
||||
private static boolean isEnumTypeName(String typeName) {
|
||||
|
||||
@@ -123,7 +123,12 @@ public class MessagingDetector {
|
||||
List<String> annotations = new ArrayList<>();
|
||||
for (Object mod : param.modifiers()) {
|
||||
if (mod instanceof Annotation ann) {
|
||||
annotations.add(ann.getTypeName().getFullyQualifiedName());
|
||||
String name = ann.getTypeName().getFullyQualifiedName();
|
||||
int lastDot = name.lastIndexOf('.');
|
||||
if (lastDot >= 0) {
|
||||
name = name.substring(lastDot + 1);
|
||||
}
|
||||
annotations.add(name);
|
||||
}
|
||||
}
|
||||
parameters.add(EntryPoint.Parameter.builder()
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
|
||||
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.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
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 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;
|
||||
|
||||
/**
|
||||
* JMS payload params must enter the same binding-expansion path as REST PathVariables.
|
||||
*/
|
||||
class JmsPayloadBindingTest {
|
||||
|
||||
@Test
|
||||
void shouldExpandJmsMessagePayloadFromStringSwitchMapper(@TempDir Path tempDir) throws Exception {
|
||||
writeJmsSwitchProject(tempDir);
|
||||
|
||||
CodebaseContext context = scan(tempDir);
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> graph =
|
||||
engine.buildCallGraph();
|
||||
|
||||
EntryPoint entryPoint = jmsEntryPoint();
|
||||
|
||||
List<Map<String, String>> variants =
|
||||
EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, graph);
|
||||
|
||||
assertThat(variants).extracting(map -> map.get("message"))
|
||||
.containsExactlyInAnyOrder("PAY", "SHIP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProduceSeparateChainsForJmsPayloadSwitchCases(@TempDir Path tempDir) throws Exception {
|
||||
writeJmsSwitchProject(tempDir);
|
||||
|
||||
CodebaseContext context = scan(tempDir);
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
|
||||
List<CallChain> chains = engine.findChains(
|
||||
List.of(jmsEntryPoint()),
|
||||
List.of(TriggerPoint.builder()
|
||||
.className("com.example.OrderService")
|
||||
.methodName("fire")
|
||||
.event("event")
|
||||
.build()));
|
||||
|
||||
assertThat(chains).hasSize(2);
|
||||
assertThat(chains.stream().map(chain -> chain.getEntryPoint().getName()))
|
||||
.containsExactlyInAnyOrder(
|
||||
"JMS: order.commands?message=PAY",
|
||||
"JMS: order.commands?message=SHIP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLinkExpandedJmsPayloadChainsToMatchingTransitions(@TempDir Path tempDir) throws Exception {
|
||||
writeJmsSwitchProject(tempDir);
|
||||
|
||||
CodebaseContext context = scan(tempDir);
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
|
||||
List<CallChain> chains = engine.findChains(
|
||||
List.of(jmsEntryPoint()),
|
||||
List.of(TriggerPoint.builder()
|
||||
.className("com.example.OrderService")
|
||||
.methodName("fire")
|
||||
.event("event")
|
||||
.build()));
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("OrderStateMachineConfig")
|
||||
.eventTypeFqn("com.example.OrderEvent")
|
||||
.transitions(List.of(
|
||||
transition("PAY", "NEW", "PAID"),
|
||||
transition("SHIP", "PAID", "SHIPPED")))
|
||||
.metadata(CodebaseMetadata.builder().callChains(chains).build())
|
||||
.build();
|
||||
|
||||
new TransitionLinkerEnricher().enrich(result, context, null);
|
||||
|
||||
List<CallChain> linked = result.getMetadata().getCallChains();
|
||||
assertThat(linked).hasSize(2);
|
||||
assertThat(linked).allSatisfy(chain -> {
|
||||
assertThat(chain.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
|
||||
assertThat(chain.getMatchedTransitions()).hasSize(1);
|
||||
});
|
||||
assertThat(linked.stream()
|
||||
.flatMap(chain -> chain.getMatchedTransitions().stream())
|
||||
.map(mt -> mt.getEvent())
|
||||
.toList())
|
||||
.containsExactlyInAnyOrder(
|
||||
"com.example.OrderEvent.PAY",
|
||||
"com.example.OrderEvent.SHIP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDetectJmsListenerEntryPoint(@TempDir Path tempDir) throws Exception {
|
||||
writeJmsSwitchProject(tempDir);
|
||||
CodebaseContext context = scan(tempDir);
|
||||
|
||||
MessagingDetector detector = new MessagingDetector(context);
|
||||
List<EntryPoint> entryPoints = context.getCompilationUnits().stream()
|
||||
.flatMap(cu -> detector.detect(cu).stream())
|
||||
.toList();
|
||||
|
||||
assertThat(entryPoints).anySatisfy(ep -> {
|
||||
assertThat(ep.getType()).isEqualTo(EntryPoint.Type.JMS);
|
||||
assertThat(ep.getName()).isEqualTo("JMS: order.commands");
|
||||
assertThat(ep.getMethodName()).isEqualTo("onMessage");
|
||||
});
|
||||
}
|
||||
|
||||
private static EntryPoint jmsEntryPoint() {
|
||||
return EntryPoint.builder()
|
||||
.type(EntryPoint.Type.JMS)
|
||||
.name("JMS: order.commands")
|
||||
.className("com.example.OrderJmsListener")
|
||||
.methodName("onMessage")
|
||||
.metadata(Map.of("destination", "order.commands", "protocol", "JMS"))
|
||||
.parameters(List.of(EntryPoint.Parameter.builder()
|
||||
.name("message")
|
||||
.type("String")
|
||||
.annotations(List.of())
|
||||
.build()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static void writeJmsSwitchProject(Path tempDir) throws Exception {
|
||||
Path javaRoot = tempDir.resolve("src/main/java/com/example");
|
||||
Files.createDirectories(javaRoot);
|
||||
Files.writeString(tempDir.resolve("build.gradle"), "plugins { id 'java' }");
|
||||
Files.writeString(javaRoot.resolve("OrderEvent.java"), """
|
||||
package com.example;
|
||||
public enum OrderEvent { PAY, SHIP, LOG }
|
||||
""");
|
||||
Files.writeString(javaRoot.resolve("OrderService.java"), """
|
||||
package com.example;
|
||||
public class OrderService {
|
||||
void fire(OrderEvent event) {}
|
||||
void dispatch(String message) {
|
||||
OrderEvent event = switch (message) {
|
||||
case "PAY" -> OrderEvent.PAY;
|
||||
case "SHIP" -> OrderEvent.SHIP;
|
||||
default -> throw new IllegalArgumentException(message);
|
||||
};
|
||||
fire(event);
|
||||
}
|
||||
}
|
||||
""");
|
||||
Files.writeString(javaRoot.resolve("OrderJmsListener.java"), """
|
||||
package com.example;
|
||||
import org.springframework.jms.annotation.JmsListener;
|
||||
public class OrderJmsListener {
|
||||
private final OrderService orderService = new OrderService();
|
||||
@JmsListener(destination = "order.commands")
|
||||
public void onMessage(String message) {
|
||||
orderService.dispatch(message);
|
||||
}
|
||||
}
|
||||
""");
|
||||
Files.writeString(javaRoot.resolve("JmsListener.java"), """
|
||||
package org.springframework.jms.annotation;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface JmsListener {
|
||||
String destination() default "";
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
private static CodebaseContext scan(Path tempDir) throws Exception {
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.setSourcepath(List.of(tempDir.resolve("src/main/java").toString()));
|
||||
context.scan(tempDir);
|
||||
return context;
|
||||
}
|
||||
|
||||
private static Transition transition(String event, String source, String target) {
|
||||
Transition transition = new Transition();
|
||||
transition.setEvent(Event.of(event, "com.example.OrderEvent." + event));
|
||||
transition.setSourceStates(List.of(State.of(source, "com.example.OrderState." + source)));
|
||||
transition.setTargetStates(List.of(State.of(target, "com.example.OrderState." + target)));
|
||||
return transition;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user