A
This commit is contained in:
@@ -88,10 +88,10 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
continue;
|
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.
|
// Resource placeholders like /{id}/pay must still link when the event is known.
|
||||||
if (tp.isExternal()
|
if (tp.isExternal()
|
||||||
&& isUnresolvedGenericRestEndpoint(chain.getEntryPoint())
|
&& isUnresolvedExternalEndpoint(chain, tp)
|
||||||
&& !hasConcreteEventEvidence(tp)) {
|
&& !hasConcreteEventEvidence(tp)) {
|
||||||
updatedChains.add(chain.toBuilder()
|
updatedChains.add(chain.toBuilder()
|
||||||
.triggerPoint(tp)
|
.triggerPoint(tp)
|
||||||
@@ -103,7 +103,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
|
|
||||||
// Clear spurious external flags (unrelated PathVariable/RequestParam) once we can link.
|
// Clear spurious external flags (unrelated PathVariable/RequestParam) once we can link.
|
||||||
if (tp.isExternal()
|
if (tp.isExternal()
|
||||||
&& (!isUnresolvedGenericRestEndpoint(chain.getEntryPoint()) || hasConcreteEventEvidence(tp))) {
|
&& (!isUnresolvedExternalEndpoint(chain, tp) || hasConcreteEventEvidence(tp))) {
|
||||||
tp = tp.toBuilder().external(false).build();
|
tp = tp.toBuilder().external(false).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +288,22 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
if (constraint == null || constraint.isBlank()) {
|
if (constraint == null || constraint.isBlank()) {
|
||||||
return constraint;
|
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*&&+", "&&");
|
||||||
stripped = stripped.replaceAll("^\\s*&&\\s*", "");
|
stripped = stripped.replaceAll("^\\s*&&\\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
|
* True when the entry path still has an unbound event-carrying placeholder
|
||||||
* ({@code {event}}, {@code {commandKey}}, …). Resource ids like {@code {id}} do not count.
|
* ({@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) {
|
private static boolean isUnresolvedGenericRestEndpoint(EntryPoint entryPoint) {
|
||||||
if (entryPoint == null) {
|
if (entryPoint == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -2262,7 +2262,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
|| typeName.endsWith("PathParam") || typeName.endsWith("QueryParam")) {
|
|| typeName.endsWith("PathParam") || typeName.endsWith("QueryParam")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (typeName.endsWith("RequestBody")) {
|
if (typeName.endsWith("RequestBody") || typeName.endsWith("Payload")) {
|
||||||
ITypeBinding typeBinding = svd.getType().resolveBinding();
|
ITypeBinding typeBinding = svd.getType().resolveBinding();
|
||||||
if (typeBinding != null && typeBinding.isEnum()) {
|
if (typeBinding != null && typeBinding.isEnum()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import java.util.regex.Matcher;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expands REST entry points whose {@code @PathVariable} parameters flow into string-keyed
|
* Expands entry points whose string parameters flow into switch/map/`valueOf` mappers
|
||||||
* switch mappers (e.g. {@code StringCommandMapper.fromString}) into concrete binding variants.
|
* 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 {
|
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.
|
* string switch/if arms and boolean ternary conditions provable in source.
|
||||||
*/
|
*/
|
||||||
public static List<Map<String, String>> expandEntryPointBindings(
|
public static List<Map<String, String>> expandEntryPointBindings(
|
||||||
@@ -57,10 +58,12 @@ public final class EntryPointBindingExpander {
|
|||||||
List<Map<String, String>> variants = new ArrayList<>();
|
List<Map<String, String>> variants = new ArrayList<>();
|
||||||
variants.add(new LinkedHashMap<>());
|
variants.add(new LinkedHashMap<>());
|
||||||
for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
|
for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
|
||||||
if (!isPathOrQueryVariable(parameter)) {
|
if (!isExpandableBindingParameter(entryPoint, parameter)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!isRequestParam(parameter) && isPlaceholderResolvedInPath(entryPoint, parameter.getName())) {
|
if (!isRequestParam(parameter)
|
||||||
|
&& !isMessagingPayloadParameter(entryPoint, parameter)
|
||||||
|
&& isPlaceholderResolvedInPath(entryPoint, parameter.getName())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
List<Map<String, String>> expanded = new ArrayList<>();
|
List<Map<String, String>> expanded = new ArrayList<>();
|
||||||
@@ -120,6 +123,7 @@ public final class EntryPointBindingExpander {
|
|||||||
metadata.put("path", path.replace(placeholder, binding.getValue()));
|
metadata.put("path", path.replace(placeholder, binding.getValue()));
|
||||||
}
|
}
|
||||||
} else if (isRequestParamParameter(entryPoint, binding.getKey())
|
} else if (isRequestParamParameter(entryPoint, binding.getKey())
|
||||||
|
|| isMessagingPayloadBinding(entryPoint, binding.getKey())
|
||||||
|| isBooleanBindingValue(binding.getValue())) {
|
|| isBooleanBindingValue(binding.getValue())) {
|
||||||
if (querySuffix.isEmpty()) {
|
if (querySuffix.isEmpty()) {
|
||||||
querySuffix.append('?');
|
querySuffix.append('?');
|
||||||
@@ -275,19 +279,90 @@ public final class EntryPointBindingExpander {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isPathOrQueryVariable(EntryPoint.Parameter parameter) {
|
private static boolean isMessagingPayloadBinding(EntryPoint entryPoint, String paramName) {
|
||||||
if (parameter.getAnnotations() == null) {
|
if (entryPoint == null || entryPoint.getParameters() == null || paramName == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return parameter.getAnnotations().stream()
|
for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
|
||||||
.anyMatch(a -> "PathVariable".equals(a) || "RequestParam".equals(a));
|
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) {
|
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 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) {
|
private static boolean isRequestParamParameter(EntryPoint entryPoint, String paramName) {
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import java.util.HashSet;
|
|||||||
import java.util.Set;
|
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 {
|
public final class ExternalTriggerPolicy {
|
||||||
|
|
||||||
@@ -79,13 +80,34 @@ public final class ExternalTriggerPolicy {
|
|||||||
return true;
|
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) {
|
if (entryMethodFqn != null && resolvedEventParamName != null) {
|
||||||
RestParamBinding binding = findMethodParameterBinding(entryMethodFqn, resolvedEventParamName, context);
|
RestParamBinding binding = findMethodParameterBinding(entryMethodFqn, resolvedEventParamName, context);
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
if (binding.pathOrQueryVariable() && !binding.enumType()) {
|
if (binding.pathOrQueryVariable() && !binding.enumType()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (binding.requestBodyEnum()) {
|
if (binding.requestBodyEnum() || binding.payloadEnum()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,6 +125,54 @@ public final class ExternalTriggerPolicy {
|
|||||||
return trigger.isExternal();
|
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) {
|
private static boolean entryPointHasUnboundPlaceholders(EntryPoint entryPoint) {
|
||||||
if (entryPoint == null) {
|
if (entryPoint == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -276,13 +346,13 @@ public final class ExternalTriggerPolicy {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isEnumRecordComponent(bodyBinding, fieldName)) {
|
if (isEnumRecordComponent(bodyBinding, fieldName)) {
|
||||||
return new RestParamBinding(false, false, true, true);
|
return new RestParamBinding(false, false, true, false, true);
|
||||||
}
|
}
|
||||||
if (isEnumDtoField(bodyBinding, fieldName, context)) {
|
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)) {
|
if (containsNestedEnumField(bodyBinding, fieldName, context)) {
|
||||||
return new RestParamBinding(false, false, true, true);
|
return new RestParamBinding(false, false, true, false, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -371,7 +441,12 @@ public final class ExternalTriggerPolicy {
|
|||||||
return false;
|
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() {
|
boolean pathOrQueryVariable() {
|
||||||
return pathVariable || requestParam;
|
return pathVariable || requestParam;
|
||||||
}
|
}
|
||||||
@@ -380,25 +455,32 @@ public final class ExternalTriggerPolicy {
|
|||||||
return requestBody && enumType;
|
return requestBody && enumType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean payloadEnum() {
|
||||||
|
return payload && enumType;
|
||||||
|
}
|
||||||
|
|
||||||
static RestParamBinding fromEntryPointParameter(EntryPoint.Parameter parameter) {
|
static RestParamBinding fromEntryPointParameter(EntryPoint.Parameter parameter) {
|
||||||
boolean pathVariable = hasAnnotation(parameter.getAnnotations(), "PathVariable");
|
boolean pathVariable = hasAnnotation(parameter.getAnnotations(), "PathVariable");
|
||||||
boolean requestParam = hasAnnotation(parameter.getAnnotations(), "RequestParam");
|
boolean requestParam = hasAnnotation(parameter.getAnnotations(), "RequestParam");
|
||||||
boolean requestBody = hasAnnotation(parameter.getAnnotations(), "RequestBody");
|
boolean requestBody = hasAnnotation(parameter.getAnnotations(), "RequestBody");
|
||||||
|
boolean payload = hasAnnotation(parameter.getAnnotations(), "Payload");
|
||||||
boolean enumType = isEnumTypeName(parameter.getType());
|
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) {
|
static RestParamBinding fromAstParameter(SingleVariableDeclaration param) {
|
||||||
boolean pathVariable = hasParameterAstAnnotation(param, "PathVariable");
|
boolean pathVariable = hasParameterAstAnnotation(param, "PathVariable");
|
||||||
boolean requestParam = hasParameterAstAnnotation(param, "RequestParam");
|
boolean requestParam = hasParameterAstAnnotation(param, "RequestParam");
|
||||||
boolean requestBody = hasParameterAstAnnotation(param, "RequestBody");
|
boolean requestBody = hasParameterAstAnnotation(param, "RequestBody");
|
||||||
|
boolean payload = hasParameterAstAnnotation(param, "Payload");
|
||||||
boolean enumType = param.getType() != null && param.getType().resolveBinding() != null
|
boolean enumType = param.getType() != null && param.getType().resolveBinding() != null
|
||||||
&& param.getType().resolveBinding().isEnum();
|
&& 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) {
|
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) {
|
private static boolean isEnumTypeName(String typeName) {
|
||||||
|
|||||||
@@ -123,7 +123,12 @@ public class MessagingDetector {
|
|||||||
List<String> annotations = new ArrayList<>();
|
List<String> annotations = new ArrayList<>();
|
||||||
for (Object mod : param.modifiers()) {
|
for (Object mod : param.modifiers()) {
|
||||||
if (mod instanceof Annotation ann) {
|
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()
|
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