diff --git a/settings.gradle b/settings.gradle index a64f302..7e272bc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,4 +15,5 @@ include ':state_machines:inheritance_extra_functions3_state_machine' include ':state_machines:simple_state_machine' include ':state_machines:extended_analysis_sample' include ':state_machines:inheritance_sample' +include ':state_machines:ultimate_ecosystem_sm' include ':state_machines:enterprise_order_system' diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/EntryPoint.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/EntryPoint.java index 29ca462..b7820b7 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/EntryPoint.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/EntryPoint.java @@ -5,6 +5,7 @@ import lombok.Builder; import lombok.Data; import lombok.extern.jackson.Jacksonized; +import java.util.List; import java.util.Map; @Data @@ -12,11 +13,23 @@ import java.util.Map; @Jacksonized @JsonIgnoreProperties(ignoreUnknown = true) public class EntryPoint { - public enum Type { REST, KAFKA, RABBIT, JMS, CUSTOM } + public enum Type { REST, KAFKA, RABBIT, JMS, SQS, SNS, CUSTOM } + + @Data + @Builder + @Jacksonized + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Parameter { + private final String name; + private final String type; + private final List annotations; + } private final Type type; private final String name; // e.g., "POST /orders" or "Kafka Topic: orders" private final String className; private final String methodName; private final Map metadata; // e.g., path, topic, exchange + @Builder.Default + private final List parameters = java.util.Collections.emptyList(); } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtIntelligenceProvider.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtIntelligenceProvider.java index 5ede38b..27d34aa 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtIntelligenceProvider.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtIntelligenceProvider.java @@ -4,13 +4,12 @@ import click.kamil.springstatemachineexporter.analysis.model.CallChain; import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver; -import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import lombok.extern.slf4j.Slf4j; import org.eclipse.jdt.core.dom.CompilationUnit; import java.nio.file.Path; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -21,6 +20,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider { private final ConstantResolver constantResolver = new ConstantResolver(); private final GenericEventDetector eventDetector; private final SpringMvcDetector mvcDetector; + private final MessagingDetector messagingDetector; private final CallGraphBuilder callGraphBuilder; private final LifecycleDetector lifecycleDetector; private final InterceptorDetector interceptorDetector; @@ -30,6 +30,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider { this.rootDir = rootDir; this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints()); this.mvcDetector = new SpringMvcDetector(context, constantResolver); + this.messagingDetector = new MessagingDetector(context); this.callGraphBuilder = new CallGraphBuilder(context); this.lifecycleDetector = new LifecycleDetector(context); this.interceptorDetector = new InterceptorDetector(context); @@ -37,7 +38,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider { @Override public List findTriggerPoints() { - List allTriggers = new java.util.ArrayList<>(); + List allTriggers = new ArrayList<>(); var cus = context.getCompilationUnits(); log.info("JdtIntelligenceProvider scanning {} compilation units for triggers", cus.size()); for (CompilationUnit cu : cus) { @@ -50,14 +51,15 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider { @Override public List findEntryPoints() { - List allEntryPoints = new java.util.ArrayList<>(); + List allEntryPoints = new ArrayList<>(); var cus = context.getCompilationUnits(); log.info("JdtIntelligenceProvider scanning {} compilation units for entry points", cus.size()); for (CompilationUnit cu : cus) { allEntryPoints.addAll(mvcDetector.detect(cu)); + allEntryPoints.addAll(messagingDetector.detect(cu)); allEntryPoints.addAll(interceptorDetector.detect(cu)); } - log.info("Found {} entry points (including interceptors) in total", allEntryPoints.size()); + log.info("Found {} entry points (including interceptors and listeners) in total", allEntryPoints.size()); return allEntryPoints; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/MessagingDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/MessagingDetector.java new file mode 100644 index 0000000..3c4f23c --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/MessagingDetector.java @@ -0,0 +1,132 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; +import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.RequiredArgsConstructor; +import org.eclipse.jdt.core.dom.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +public class MessagingDetector { + private final CodebaseContext context; + private final ConstantResolver constantResolver = new ConstantResolver(); + + public List detect(CompilationUnit cu) { + List entryPoints = new ArrayList<>(); + cu.accept(new ASTVisitor() { + @Override + public boolean visit(MethodDeclaration node) { + processMethod(node, entryPoints); + return super.visit(node); + } + }); + return entryPoints; + } + + private void processMethod(MethodDeclaration method, List entryPoints) { + for (Object modifier : method.modifiers()) { + if (modifier instanceof Annotation annotation) { + String typeName = annotation.getTypeName().getFullyQualifiedName(); + + EntryPoint.Type type = null; + String destination = ""; + + if (typeName.endsWith("JmsListener")) { + type = EntryPoint.Type.JMS; + destination = resolveValue(annotation, "destination"); + } else if (typeName.endsWith("RabbitListener")) { + type = EntryPoint.Type.RABBIT; + destination = resolveValue(annotation, "queues"); + } else if (typeName.endsWith("SqsListener")) { + type = EntryPoint.Type.SQS; + destination = resolveValue(annotation, "value"); + if ("unknown".equals(destination)) { + destination = resolveValue(annotation, "queueNames"); + } + } else if (typeName.endsWith("SnsListener")) { + type = EntryPoint.Type.SNS; + destination = resolveValue(annotation, "value"); + if ("unknown".equals(destination)) { + destination = resolveValue(annotation, "topicNames"); + } + } else if (typeName.endsWith("KafkaListener")) { + type = EntryPoint.Type.KAFKA; + destination = resolveValue(annotation, "topics"); + } + + if (type != null) { + Map metadata = new HashMap<>(); + metadata.put("destination", destination); + + String protocolName = type.name(); + if (typeName.endsWith("SqsListener")) protocolName = "SQS"; + if (typeName.endsWith("SnsListener")) protocolName = "SNS"; + metadata.put("protocol", protocolName); + + entryPoints.add(EntryPoint.builder() + .type(type) + .name(protocolName + ": " + destination) + .className(context.getFqn((TypeDeclaration) method.getParent())) + .methodName(method.getName().getIdentifier()) + .metadata(metadata) + .parameters(extractParameters(method)) + .build()); + } + } + } + } + + private String resolveValue(Annotation annotation, String memberName) { + Expression value = null; + if (annotation instanceof SingleMemberAnnotation sma) { + value = sma.getValue(); + } else if (annotation instanceof NormalAnnotation na) { + for (Object pairObj : na.values()) { + MemberValuePair pair = (MemberValuePair) pairObj; + if (memberName.equals(pair.getName().getIdentifier())) { + value = pair.getValue(); + break; + } + } + } + + if (value != null) { + if (value instanceof ArrayInitializer ai) { + List values = new ArrayList<>(); + for (Object o : ai.expressions()) { + String resolved = constantResolver.resolve((Expression) o, context); + values.add(resolved != null ? resolved : o.toString()); + } + return String.join(", ", values); + } + String resolved = constantResolver.resolve(value, context); + return resolved != null ? resolved : value.toString(); + } + return "unknown"; + } + + private List extractParameters(MethodDeclaration method) { + List parameters = new ArrayList<>(); + for (Object paramObj : method.parameters()) { + if (paramObj instanceof SingleVariableDeclaration param) { + List annotations = new ArrayList<>(); + for (Object mod : param.modifiers()) { + if (mod instanceof Annotation ann) { + annotations.add(ann.getTypeName().getFullyQualifiedName()); + } + } + parameters.add(EntryPoint.Parameter.builder() + .name(param.getName().getIdentifier()) + .type(param.getType().toString()) + .annotations(annotations) + .build()); + } + } + return parameters; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringMvcDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringMvcDetector.java index 86f6452..62128df 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringMvcDetector.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringMvcDetector.java @@ -36,7 +36,7 @@ public class SpringMvcDetector { for (Object modifier : node.modifiers()) { if (modifier instanceof Annotation annotation) { String name = annotation.getTypeName().getFullyQualifiedName(); - if (name.endsWith("RestController") || name.endsWith("Controller")) { + if (name.endsWith("RestController") || name.endsWith("Controller") || name.endsWith("Path")) { return true; } } @@ -46,6 +46,10 @@ public class SpringMvcDetector { private void processController(TypeDeclaration controller, List entryPoints) { String basePath = getMappingPathHierarchical(controller); + if (basePath.isEmpty()) { + basePath = getJaxRsPath(controller); + } + for (MethodDeclaration method : controller.getMethods()) { EntryPoint ep = processMethodHierarchical(method, basePath, controller); if (ep != null) { @@ -55,11 +59,9 @@ public class SpringMvcDetector { } private String getMappingPathHierarchical(TypeDeclaration td) { - // 1. Check class itself String path = getMappingPath(td); if (!path.isEmpty()) return path; - // 2. Check interfaces for (Object interfaceType : td.superInterfaceTypes()) { TypeDeclaration itd = context.getTypeDeclaration(interfaceType.toString()); if (itd != null) { @@ -68,7 +70,6 @@ public class SpringMvcDetector { } } - // 3. Check superclass if (td.getSuperclassType() != null) { TypeDeclaration std = context.getTypeDeclaration(td.getSuperclassType().toString()); if (std != null) { @@ -80,11 +81,9 @@ public class SpringMvcDetector { } private EntryPoint processMethodHierarchical(MethodDeclaration method, String basePath, TypeDeclaration controller) { - // 1. Check the method itself EntryPoint ep = processMethod(method, basePath, controller); if (ep != null) return ep; - // 2. Check interface methods String methodName = method.getName().getIdentifier(); for (Object interfaceType : controller.superInterfaceTypes()) { TypeDeclaration itd = context.getTypeDeclaration(interfaceType.toString()); @@ -108,18 +107,24 @@ public class SpringMvcDetector { String verb = getHttpVerb(name); if (verb != null) { String methodPath = getMappingPath(annotation); + if (methodPath.isEmpty()) { + methodPath = getJaxRsPath(method); + } String fullPath = combinePaths(basePath, methodPath); Map metadata = new HashMap<>(); metadata.put("path", fullPath); metadata.put("verb", verb); + List parameters = extractParameters(method); + return EntryPoint.builder() .type(EntryPoint.Type.REST) .name(verb + " " + fullPath) .className(context.getFqn(controller)) .methodName(method.getName().getIdentifier()) .metadata(metadata) + .parameters(parameters) .build(); } } @@ -127,12 +132,21 @@ public class SpringMvcDetector { return null; } + private String getJaxRsPath(BodyDeclaration node) { + for (Object mod : node.modifiers()) { + if (mod instanceof Annotation ann && ann.getTypeName().getFullyQualifiedName().endsWith("Path")) { + return getMappingPath(ann); + } + } + return ""; + } + private String getHttpVerb(String annotationName) { - if (annotationName.endsWith("PostMapping")) return "POST"; - if (annotationName.endsWith("GetMapping")) return "GET"; - if (annotationName.endsWith("PutMapping")) return "PUT"; - if (annotationName.endsWith("DeleteMapping")) return "DELETE"; - if (annotationName.endsWith("PatchMapping")) return "PATCH"; + if (annotationName.endsWith("PostMapping") || annotationName.endsWith("POST")) return "POST"; + if (annotationName.endsWith("GetMapping") || annotationName.endsWith("GET")) return "GET"; + if (annotationName.endsWith("PutMapping") || annotationName.endsWith("PUT")) return "PUT"; + if (annotationName.endsWith("DeleteMapping") || annotationName.endsWith("DELETE")) return "DELETE"; + if (annotationName.endsWith("PatchMapping") || annotationName.endsWith("PATCH")) return "PATCH"; if (annotationName.endsWith("RequestMapping")) return "REQUEST"; return null; } @@ -140,7 +154,8 @@ public class SpringMvcDetector { private String getMappingPath(TypeDeclaration td) { for (Object modifier : td.modifiers()) { if (modifier instanceof Annotation annotation) { - if (annotation.getTypeName().getFullyQualifiedName().endsWith("RequestMapping")) { + String name = annotation.getTypeName().getFullyQualifiedName(); + if (name.endsWith("RequestMapping") || name.endsWith("Path")) { return getMappingPath(annotation); } } @@ -169,6 +184,29 @@ public class SpringMvcDetector { return ""; } + private List extractParameters(MethodDeclaration method) { + List parameters = new ArrayList<>(); + for (Object paramObj : method.parameters()) { + if (paramObj instanceof SingleVariableDeclaration param) { + List annotations = new ArrayList<>(); + for (Object mod : param.modifiers()) { + if (mod instanceof Annotation ann) { + String name = ann.getTypeName().getFullyQualifiedName(); + if (name.endsWith("PathParam")) name = "PathVariable"; + if (name.endsWith("QueryParam")) name = "RequestParam"; + annotations.add(name); + } + } + parameters.add(EntryPoint.Parameter.builder() + .name(param.getName().getIdentifier()) + .type(param.getType().toString()) + .annotations(annotations) + .build()); + } + } + return parameters; + } + private String combinePaths(String base, String method) { String b = base.startsWith("/") ? base : "/" + base; if (b.endsWith("/")) b = b.substring(0, b.length() - 1); diff --git a/state_machine_exporter_html/out_enterprise/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig.html b/state_machine_exporter_html/out_enterprise/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig.html index 4f5b783..a5b4052 100644 --- a/state_machine_exporter_html/out_enterprise/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig.html +++ b/state_machine_exporter_html/out_enterprise/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig.html @@ -98,13 +98,13 @@ display: block; } - /* SVG Highlighting - Sleek Technical Tracers */ - svg .stereotype { display: none !important; } - - .active-path path { + /* SVG Interactivity Styles */ + svg a { text-decoration: none !important; } + + .active-path { stroke: var(--accent) !important; - stroke-width: 2.0px !important; - filter: drop-shadow(0 0 6px rgba(239, 68, 68, 0.3)); + stroke-width: 2.5px !important; + filter: drop-shadow(0 0 8px rgba(239, 68, 68, 0.3)); transition: all 0.3s; } @@ -117,7 +117,7 @@ } .dimmed { - opacity: 0.25 !important; + opacity: 0.2 !important; filter: grayscale(1); transition: opacity 0.4s ease; pointer-events: none; @@ -151,6 +151,11 @@ line-height: 1.5; margin-top: 10px; } + + .payload-param { + margin-bottom: 4px; + } + .payload-param:last-child { margin-bottom: 0; } @@ -344,7 +349,8 @@ "metadata" : { "path" : "/api/enterprise/orders/place", "verb" : "POST" - } + }, + "parameters" : [ ] }, { "type" : "REST", "name" : "POST /api/enterprise/orders/{id}/cancel", @@ -353,7 +359,12 @@ "metadata" : { "path" : "/api/enterprise/orders/{id}/cancel", "verb" : "POST" - } + }, + "parameters" : [ { + "name" : "id", + "type" : "String", + "annotations" : [ "PathVariable" ] + } ] }, { "type" : "CUSTOM", "name" : "INTERCEPTOR: SecurityInterceptor.preHandle", @@ -361,7 +372,8 @@ "methodName" : "preHandle", "metadata" : { "interceptorType" : "Spring MVC Interceptor" - } + }, + "parameters" : [ ] }, { "type" : "REST", "name" : "POST /api/enterprise/payments", @@ -370,7 +382,12 @@ "metadata" : { "path" : "/api/enterprise/payments", "verb" : "POST" - } + }, + "parameters" : [ { + "name" : "orderId", + "type" : "String", + "annotations" : [ "RequestBody" ] + } ] } ], "callChains" : [ { "entryPoint" : { @@ -381,7 +398,8 @@ "metadata" : { "path" : "/api/enterprise/orders/place", "verb" : "POST" - } + }, + "parameters" : [ ] }, "methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.placeOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.place" ], "triggerPoint" : { @@ -401,7 +419,12 @@ "metadata" : { "path" : "/api/enterprise/orders/{id}/cancel", "verb" : "POST" - } + }, + "parameters" : [ { + "name" : "id", + "type" : "String", + "annotations" : [ "PathVariable" ] + } ] }, "methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.cancelOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.cancel" ], "triggerPoint" : { @@ -420,7 +443,8 @@ "methodName" : "preHandle", "metadata" : { "interceptorType" : "Spring MVC Interceptor" - } + }, + "parameters" : [ ] }, "methodChain" : [ "click.kamil.examples.enterprise.api.SecurityInterceptor.preHandle" ], "triggerPoint" : { @@ -440,7 +464,12 @@ "metadata" : { "path" : "/api/enterprise/payments", "verb" : "POST" - } + }, + "parameters" : [ { + "name" : "orderId", + "type" : "String", + "annotations" : [ "RequestBody" ] + } ] }, "methodChain" : [ "click.kamil.examples.enterprise.api.ReactivePaymentController.pay", "click.kamil.examples.enterprise.service.ReactivePaymentService.processPayment" ], "triggerPoint" : { @@ -500,7 +529,7 @@ const svg = document.querySelector('svg'); - // Dim all shapes and texts + // Dim everything first svg.querySelectorAll('path, rect, circle, ellipse, text, polygon').forEach(el => { el.classList.add('dimmed'); }); @@ -513,7 +542,7 @@ svg.querySelectorAll(`a[*|href="${linkId}"], a[href="${linkId}"]`).forEach(link => { link.classList.add('active-path'); link.querySelectorAll('*').forEach(child => child.classList.remove('dimmed')); - + // Surgical Selection: Grab ONLY the immediate path and polygon let prev = link.previousElementSibling; let foundPath = false; @@ -618,24 +647,40 @@ if (chains && chains.length > 0) { html += `
Triggered by
`; chains.forEach(c => { - html += `
+ html += `
${c.entryPoint.name}
-
+ + ${renderParameters(c.entryPoint.parameters)} + +
${c.methodChain.map((m, i) => `
${i+1} ${m}
`).join('')}
`; }); } - // Placeholder for Data Object - html += `
Data Payload (Planned)
-
- OrderRequest {
-   // Full AST field mapping to be implemented
-   id: "ORD-123",
-   quantity: 5
- } -
`; + html += `
`; + return html; + } + + function renderParameters(params) { + if (!params || params.length === 0) return ""; + + let html = `
Input Data
`; + html += `
`; + + params.forEach(p => { + const annotation = (p.annotations || []).find(a => a.endsWith("RequestBody") || a.endsWith("PathVariable") || a.endsWith("RequestParam")); + const cleanAnn = annotation ? "@" + annotation.substring(annotation.lastIndexOf('.') + 1) : ""; + + html += `
+ ${cleanAnn} + ${p.type} + ${p.name} +
`; + }); + + html += `
`; return html; } diff --git a/state_machine_exporter_html/out_ultimate/click.kamil.ultimate.config.UltimateStateMachineConfig/click.kamil.ultimate.config.UltimateStateMachineConfig.html b/state_machine_exporter_html/out_ultimate/click.kamil.ultimate.config.UltimateStateMachineConfig/click.kamil.ultimate.config.UltimateStateMachineConfig.html new file mode 100644 index 0000000..3cb11e0 --- /dev/null +++ b/state_machine_exporter_html/out_ultimate/click.kamil.ultimate.config.UltimateStateMachineConfig/click.kamil.ultimate.config.UltimateStateMachineConfig.html @@ -0,0 +1,967 @@ + + + + + + State Machine Explorer - click.kamil.ultimate.config.UltimateStateMachineConfig + + + + + + + + + + + + + + + + + + + + + + + diff --git a/state_machine_exporter_html/src/main/resources/html/template.html b/state_machine_exporter_html/src/main/resources/html/template.html index be314b6..b36d442 100644 --- a/state_machine_exporter_html/src/main/resources/html/template.html +++ b/state_machine_exporter_html/src/main/resources/html/template.html @@ -79,6 +79,10 @@ .type-rest { background: #eff6ff; color: #1d4ed8; } .type-custom { background: #fef2f2; color: #b91c1c; } .type-jms { background: #f5f3ff; color: #7c3aed; } + .type-sqs { background: #fff7ed; color: #ea580c; } + .type-sns { background: #fff1f2; color: #e11d48; } + .type-kafka { background: #fefce8; color: #854d0e; } + .type-rabbit { background: #f0fdf4; color: #166534; } .ep-name { font-weight: 700; font-size: 0.95rem; margin-bottom: 6px; color: var(--text); } .ep-fqn { font-family: 'JetBrains Mono', monospace; font-size: 0.65rem; color: var(--text-muted); opacity: 0.8; } @@ -98,13 +102,13 @@ display: block; } - /* SVG Highlighting - Sleek Technical Tracers */ - svg .stereotype { display: none !important; } - - .active-path path { + /* SVG Interactivity Styles */ + svg a { text-decoration: none !important; } + + .active-path { stroke: var(--accent) !important; - stroke-width: 2.0px !important; - filter: drop-shadow(0 0 6px rgba(239, 68, 68, 0.3)); + stroke-width: 2.5px !important; + filter: drop-shadow(0 0 8px rgba(239, 68, 68, 0.3)); transition: all 0.3s; } @@ -117,7 +121,7 @@ } .dimmed { - opacity: 0.25 !important; + opacity: 0.2 !important; filter: grayscale(1); transition: opacity 0.4s ease; pointer-events: none; @@ -151,6 +155,11 @@ line-height: 1.5; margin-top: 10px; } + + .payload-param { + margin-bottom: 4px; + } + .payload-param:last-child { margin-bottom: 0; } @@ -217,7 +226,7 @@ const svg = document.querySelector('svg'); - // Dim all shapes and texts + // Dim everything first svg.querySelectorAll('path, rect, circle, ellipse, text, polygon').forEach(el => { el.classList.add('dimmed'); }); @@ -230,7 +239,7 @@ svg.querySelectorAll(`a[*|href="${linkId}"], a[href="${linkId}"]`).forEach(link => { link.classList.add('active-path'); link.querySelectorAll('*').forEach(child => child.classList.remove('dimmed')); - + // Surgical Selection: Grab ONLY the immediate path and polygon let prev = link.previousElementSibling; let foundPath = false; @@ -335,24 +344,40 @@ if (chains && chains.length > 0) { html += `
Triggered by
`; chains.forEach(c => { - html += `
+ html += `
${c.entryPoint.name}
-
+ + ${renderParameters(c.entryPoint.parameters)} + +
${c.methodChain.map((m, i) => `
${i+1} ${m}
`).join('')}
`; }); } - // Placeholder for Data Object - html += `
Data Payload (Planned)
-
- OrderRequest {
-   // Full AST field mapping to be implemented
-   id: "ORD-123",
-   quantity: 5
- } -
`; + html += `
`; + return html; + } + + function renderParameters(params) { + if (!params || params.length === 0) return ""; + + let html = `
Input Data
`; + html += `
`; + + params.forEach(p => { + const annotation = (p.annotations || []).find(a => a.endsWith("RequestBody") || a.endsWith("PathVariable") || a.endsWith("RequestParam")); + const cleanAnn = annotation ? "@" + annotation.substring(annotation.lastIndexOf('.') + 1) : ""; + + html += `
+ ${cleanAnn} + ${p.type} + ${p.name} +
`; + }); + + html += `
`; return html; } diff --git a/state_machines/ultimate_ecosystem_sm/build.gradle b/state_machines/ultimate_ecosystem_sm/build.gradle new file mode 100644 index 0000000..f43a826 --- /dev/null +++ b/state_machines/ultimate_ecosystem_sm/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.5.3' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'click.kamil' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.statemachine:spring-statemachine-starter:4.0.0' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-activemq' + implementation 'org.springframework.boot:spring-boot-starter-amqp' + implementation 'org.springframework.boot:spring-boot-starter-jersey' + + // AWS SQS/SNS + implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs:3.1.1' + implementation 'io.awspring.cloud:spring-cloud-aws-starter-sns:3.1.1' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' +} + +tasks.named('test') { + useJUnitPlatform() +} + +bootJar { enabled = false } +jar { enabled = true } diff --git a/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/api/JerseyResource.java b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/api/JerseyResource.java new file mode 100644 index 0000000..0ae19f3 --- /dev/null +++ b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/api/JerseyResource.java @@ -0,0 +1,26 @@ +package click.kamil.ultimate.api; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import lombok.RequiredArgsConstructor; +import org.springframework.statemachine.StateMachine; +import org.springframework.stereotype.Component; + +@Path("/jaxrs/ticket") +@Component +@RequiredArgsConstructor +public class JerseyResource { + private final StateMachine stateMachine; + + @POST + @Consumes(MediaType.APPLICATION_JSON) + public void createJaxRs(String payload) { + stateMachine.sendEvent("CREATE_JAXRS"); + } + + @PUT + @Path("/{id}/resolve") + public void resolve(@PathParam("id") String id) { + stateMachine.sendEvent("RESOLVE"); + } +} diff --git a/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/config/UltimateStateMachineConfig.java b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/config/UltimateStateMachineConfig.java new file mode 100644 index 0000000..846e12b --- /dev/null +++ b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/config/UltimateStateMachineConfig.java @@ -0,0 +1,60 @@ +package click.kamil.ultimate.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +import java.util.EnumSet; + +@Configuration +@EnableStateMachine +public class UltimateStateMachineConfig extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("NEW") + .state("TRIAGE") + .state("IN_PROGRESS") + .state("ESCALATED") + .end("RESOLVED") + .end("CLOSED"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("NEW").target("TRIAGE").event("CREATE") + .and() + .withExternal() + .source("NEW").target("IN_PROGRESS").event("CREATE_REACTIVE") + .and() + .withExternal() + .source("NEW").target("IN_PROGRESS").event("CREATE_JAXRS") + .and() + .withExternal() + .source("TRIAGE").target("IN_PROGRESS").event("ASSIGN") + .and() + .withExternal() + .source("IN_PROGRESS").target("ESCALATED").event("ESCALATE") + .and() + .withExternal() + .source("IN_PROGRESS").target("RESOLVED").event("RESOLVE") + .and() + .withExternal() + .source("ESCALATED").target("RESOLVED").event("RESOLVE") + .and() + .withExternal() + .source("RESOLVED").target("CLOSED").event("CLOSE_TICKET") + .and() + .withExternal() + .source("IN_PROGRESS").target("TRIAGE").event("SNS_UPDATE") + .and() + .withExternal() + .source("TRIAGE").target("ESCALATED").event("SQS_UPDATE"); + } +} diff --git a/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/messaging/JmsTicketListener.java b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/messaging/JmsTicketListener.java new file mode 100644 index 0000000..733b5eb --- /dev/null +++ b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/messaging/JmsTicketListener.java @@ -0,0 +1,17 @@ +package click.kamil.ultimate.messaging; + +import lombok.RequiredArgsConstructor; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.statemachine.StateMachine; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class JmsTicketListener { + private final StateMachine stateMachine; + + @JmsListener(destination = "ticket.close.queue") + public void onCloseMessage(String ticketId) { + stateMachine.sendEvent("CLOSE_TICKET"); + } +} diff --git a/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/messaging/RabbitTicketListener.java b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/messaging/RabbitTicketListener.java new file mode 100644 index 0000000..c2c729a --- /dev/null +++ b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/messaging/RabbitTicketListener.java @@ -0,0 +1,17 @@ +package click.kamil.ultimate.messaging; + +import lombok.RequiredArgsConstructor; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.statemachine.StateMachine; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RabbitTicketListener { + private final StateMachine stateMachine; + + @RabbitListener(queues = "ticket.escalate.exchange") + public void onEscalate(String payload) { + stateMachine.sendEvent("ESCALATE"); + } +} diff --git a/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/messaging/SnsTicketListener.java b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/messaging/SnsTicketListener.java new file mode 100644 index 0000000..6d576ae --- /dev/null +++ b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/messaging/SnsTicketListener.java @@ -0,0 +1,17 @@ +package click.kamil.ultimate.messaging; + +import io.awspring.cloud.sns.annotation.SnsListener; +import lombok.RequiredArgsConstructor; +import org.springframework.statemachine.StateMachine; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SnsTicketListener { + private final StateMachine stateMachine; + + @SnsListener("ticket-sns-topic") + public void onSnsMessage(String subject, String message) { + stateMachine.sendEvent("SNS_UPDATE"); + } +} diff --git a/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/messaging/SqsTicketListener.java b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/messaging/SqsTicketListener.java new file mode 100644 index 0000000..3169c39 --- /dev/null +++ b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/messaging/SqsTicketListener.java @@ -0,0 +1,17 @@ +package click.kamil.ultimate.messaging; + +import io.awspring.cloud.sqs.annotation.SqsListener; +import lombok.RequiredArgsConstructor; +import org.springframework.statemachine.StateMachine; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SqsTicketListener { + private final StateMachine stateMachine; + + @SqsListener("ticket-updates-queue") + public void onUpdate(String message) { + stateMachine.sendEvent("SQS_UPDATE"); + } +} diff --git a/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/model/TicketRequest.java b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/model/TicketRequest.java new file mode 100644 index 0000000..b102997 --- /dev/null +++ b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/model/TicketRequest.java @@ -0,0 +1,12 @@ +package click.kamil.ultimate.model; + +import lombok.Data; + +@Data +public class TicketRequest { + private String subject; + private String description; + private Priority priority; + + public enum Priority { LOW, MEDIUM, HIGH } +} diff --git a/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/web/ReactiveController.java b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/web/ReactiveController.java new file mode 100644 index 0000000..8baa638 --- /dev/null +++ b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/web/ReactiveController.java @@ -0,0 +1,21 @@ +package click.kamil.ultimate.web; + +import lombok.RequiredArgsConstructor; +import org.springframework.statemachine.StateMachine; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RestController +@RequestMapping("/api/ticket/reactive") +@RequiredArgsConstructor +public class ReactiveController { + private final StateMachine stateMachine; + + @PostMapping + public Mono createReactive(@RequestBody String data) { + return Mono.fromRunnable(() -> stateMachine.sendEvent("CREATE_REACTIVE")); + } +} diff --git a/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/web/WebController.java b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/web/WebController.java new file mode 100644 index 0000000..5646161 --- /dev/null +++ b/state_machines/ultimate_ecosystem_sm/src/main/java/click/kamil/ultimate/web/WebController.java @@ -0,0 +1,23 @@ +package click.kamil.ultimate.web; + +import click.kamil.ultimate.model.TicketRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.statemachine.StateMachine; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/ticket") +@RequiredArgsConstructor +public class WebController { + private final StateMachine stateMachine; + + @PostMapping("/create") + public void createTicket(@RequestBody TicketRequest request) { + stateMachine.sendEvent("CREATE"); + } + + @PostMapping("/{id}/assign") + public void assignTicket(@PathVariable String id, @RequestParam String userId) { + stateMachine.sendEvent("ASSIGN"); + } +}