diff --git a/plan-extended-anaylis/PLAN.md b/plan-extended-anaylis/PLAN.md index 81be21f..bdaebbe 100644 --- a/plan-extended-anaylis/PLAN.md +++ b/plan-extended-anaylis/PLAN.md @@ -25,9 +25,16 @@ - [ ] **ConfigurableTriggerDetector**: Support user-defined patterns for custom frameworks. ## Phase 4: Call Graph Integration -- [ ] Develop a call graph builder to link `Entry Points` to `Trigger Points` via intermediate service calls. +- [x] Develop a call graph builder to link `Entry Points` to `Trigger Points` via intermediate service calls. - [ ] Support inheritance in call graph resolution (using JDT bindings to resolve method overrides). +## Phase 4.5: Hierarchical Robustness (Inheritance Support) +- [ ] **Hierarchical Annotation Lookup**: Update `SpringMvcDetector` to scan interfaces and superclasses for inherited mapping annotations. +- [ ] **Interface Implementation Mapping**: Enhance `CodebaseContext` to index "Class -> Interfaces" and "Interface -> Classes" relationships. +- [ ] **Polymorphic Call Trace**: Update `CallGraphBuilder` to follow calls from an interface method to all known implementations in the project. +- [ ] **Base Class Trigger Discovery**: Ensure triggers in abstract base classes are correctly attributed to their concrete subclasses. + + ## Phase 5: Visualization & Diagnostics - [ ] Update `ExportService` to include trigger information. - [ ] Update DOT/SCXML exporters to show entry points. @@ -39,9 +46,11 @@ - [ ] Verify that inheritance in both state machine config and controllers is correctly handled. ## Phase 7: Advanced Resolution & Data Flow -- [ ] **Complex ValueResolver**: Handle string concatenations and variable references in annotations/logic. +- [ ] **Variable Propagation**: Track values from fields/constructors to their usage in the State Machine configuration (e.g., resolving `initial(initialState)` where `initialState` is a field). +- [ ] **@Value Resolution**: Link `@Value` annotations to the `PropertyResolver` to substitute placeholders with literal values in the diagram. +- [ ] **Spring Profile Support**: Update `PropertyResolver` to handle `application-{profile}.properties` and prioritize them based on an "active profiles" setting. +- [ ] **Complex ValueResolver**: Handle string concatenations (e.g., `"PREFIX_" + MyConstants.SUFFIX`) and variable references. - [ ] **Payload Analysis**: Extract payload types from entry points and track field usage in `sendEvent` calls. -- [ ] **@Value Propagation**: Track property values injected into fields and constructors. ## Phase 8: Persistence & Lifecycle Mapping - [ ] Detect persistence restoration logic (`persister.restore`). @@ -53,5 +62,10 @@ - [ ] **Interceptors & Filters**: Detect `HandlerInterceptor` and `Filter` implementations and map them to endpoints. ## Phase 10: External Dependency Interop -- [ ] **Library Hints**: Allow users to provide a JSON map of "Method -> Event" for third-party libraries where source is unavailable. +- [x] **Library Hints**: Implementation complete. Supports `hints.json` in project root or `src/main/resources/`. + - **Format**: `[{"methodFqn": "class.method", "event": "EVENT"}]` + - **Purpose**: Maps external/compiled library calls to State Machine events when source code is unavailable. - [ ] **External JAR Analysis**: (Optional) Configure JDT to scan source-attachments of dependencies if available. + +**Note on Library Hints**: This feature is an optional "Power User" escape hatch. The analyzer works automatically for all local source code; `hints.json` is only required to bridge the gap for third-party libraries. + diff --git a/settings.gradle b/settings.gradle index 1d6131b..1d6ebc0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,3 +12,6 @@ include ':state_machines:inheritance_extra_functions_state_machine' include ':state_machines:inheritance_extra_functions2_state_machine' 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:enterprise_order_system' diff --git a/state_machine_exporter/out_simple/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration.dot b/state_machine_exporter/out_simple/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration.dot deleted file mode 100644 index f86efca..0000000 --- a/state_machine_exporter/out_simple/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration.dot +++ /dev/null @@ -1,29 +0,0 @@ -digraph statemachine { - rankdir=LR; - node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"]; - edge [fontname="Arial", fontsize=10]; - - SUBMITTED_choice [shape=diamond, label="", fillcolor="blue", width=0.2, height=0.2]; - SUBMITTED -> SUBMITTED_choice [style=dotted, color="blue"]; - PAID_choice [shape=diamond, label="", fillcolor="green", width=0.2, height=0.2]; - PAID -> PAID_choice [style=dotted, color="green"]; - _start [shape=circle, label="", fillcolor=black, width=0.1]; - _start -> SUBMITTED; - CANCELED [fillcolor=lightgray]; - FULFILLED [fillcolor=lightgray]; - SUBMITTED -> PAID [label="OrderEvents.PAY", style="solid", color="black"]; - PAID -> FULFILLED [label="OrderEvents.FULFILL", style="solid", color="black"]; - SUBMITTED -> CANCELED [label="OrderEvents.CANCEL [λ]", style="solid", color="black"]; - PAID -> CANCELED [label="OrderEvents.CANCEL", style="solid", color="black"]; - SUBMITTED -> SUBMITTED [label="OrderEvents.ABCD", style="solid", color="black"]; - SUBMITTED_choice -> PAID2 [label="[λ] (order=0)", style="solid", color="blue"]; - SUBMITTED_choice -> PAID3 [label="(order=1)", style="solid", color="blue"]; - PAID_choice -> PAID1 [label="[λ] (order=0)", style="solid", color="green"]; - PAID_choice -> PAID2 [label="[guard1] (order=1)", style="solid", color="green"]; - PAID_choice -> HAPPEN [label="[λ] (order=2)", style="solid", color="green"]; - PAID_choice -> PAID3 [label="(order=3)", style="solid", color="green"]; - PAID2 -> CANCELED [style="solid", color="black"]; - PAID3 -> CANCELED [style="solid", color="black"]; - PAID1 -> PAID1 [label="OrderEvents.ABCD / λ, action2", style="solid", color="black"]; -} - diff --git a/state_machine_exporter/out_simple/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration.scxml.xml b/state_machine_exporter/out_simple/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration.scxml.xml deleted file mode 100644 index 57145e3..0000000 --- a/state_machine_exporter/out_simple/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration.scxml.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java index 76e464c..6609459 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java @@ -16,8 +16,7 @@ public class Main { public static void main(String[] args) { // Manual DI / Wiring var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter()); - var enrichmentService = new EnrichmentService(Collections.emptyList()); - var exportService = new ExportService(exporters, enrichmentService); + var exportService = new ExportService(exporters); var command = new ExporterCommand(exportService); int exitCode = new CommandLine(command).execute(args); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/AnalysisEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/AnalysisEnricher.java index 2a6fd21..c9a6b9a 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/AnalysisEnricher.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/AnalysisEnricher.java @@ -1,8 +1,9 @@ package click.kamil.springstatemachineexporter.analysis.enricher; import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; +import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; public interface AnalysisEnricher { - void enrich(AnalysisResult result, CodebaseContext context); + void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence); } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java new file mode 100644 index 0000000..ad3b361 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java @@ -0,0 +1,28 @@ +package click.kamil.springstatemachineexporter.analysis.enricher; + +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.service.CodebaseIntelligenceProvider; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class CallChainEnricher implements AnalysisEnricher { + + @Override + public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) { + log.info("Enriching {} with call chains", result.getName()); + + List chains = intelligence.findCallChains( + result.getMetadata().getEntryPoints(), + result.getMetadata().getTriggers() + ); + + result.addMetadata(CodebaseMetadata.builder() + .callChains(chains) + .build()); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/EntryPointEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/EntryPointEnricher.java new file mode 100644 index 0000000..873cbce --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/EntryPointEnricher.java @@ -0,0 +1,25 @@ +package click.kamil.springstatemachineexporter.analysis.enricher; + +import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; +import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata; +import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; +import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class EntryPointEnricher implements AnalysisEnricher { + + @Override + public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) { + log.info("Enriching {} with entry points", result.getName()); + + List entryPoints = intelligence.findEntryPoints(); + + result.addMetadata(CodebaseMetadata.builder() + .entryPoints(entryPoints) + .build()); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/PropertyEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/PropertyEnricher.java new file mode 100644 index 0000000..bbae292 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/PropertyEnricher.java @@ -0,0 +1,24 @@ +package click.kamil.springstatemachineexporter.analysis.enricher; + +import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; +import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata; +import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +@Slf4j +public class PropertyEnricher implements AnalysisEnricher { + + @Override + public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) { + log.info("Enriching {} with properties", result.getName()); + + Map properties = intelligence.resolveProperties(); + + result.addMetadata(CodebaseMetadata.builder() + .properties(properties) + .build()); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerEnricher.java new file mode 100644 index 0000000..44ef006 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerEnricher.java @@ -0,0 +1,26 @@ +package click.kamil.springstatemachineexporter.analysis.enricher; + +import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class TriggerEnricher implements AnalysisEnricher { + + @Override + public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) { + log.info("Enriching {} with triggers", result.getName()); + + List triggers = intelligence.findTriggerPoints(); + + // Initially, we just add all triggers found in the codebase to the metadata. + // In later steps of Phase 2, we will filter them by State Machine ID. + result.addMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder() + .triggers(triggers) + .build()); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java index 6a0533f..eb8fc7b 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java @@ -1,8 +1,12 @@ package click.kamil.springstatemachineexporter.analysis.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import click.kamil.springstatemachineexporter.model.Transition; import lombok.Builder; import lombok.Getter; +import lombok.extern.jackson.Jacksonized; import java.util.Collections; import java.util.List; @@ -10,6 +14,8 @@ import java.util.Set; @Getter @Builder +@Jacksonized +@JsonIgnoreProperties(ignoreUnknown = true) public class AnalysisResult { private final String name; private final List transitions; @@ -23,18 +29,22 @@ public class AnalysisResult { public void addMetadata(CodebaseMetadata newMetadata) { if (newMetadata == null) return; - List combinedTriggers = new java.util.ArrayList<>(this.metadata.getTriggers()); - combinedTriggers.addAll(newMetadata.getTriggers()); + List combinedTriggers = new java.util.ArrayList<>(this.metadata.getTriggers()); + if (newMetadata.getTriggers() != null) combinedTriggers.addAll(newMetadata.getTriggers()); - List combinedEntryPoints = new java.util.ArrayList<>(this.metadata.getEntryPoints()); - combinedEntryPoints.addAll(newMetadata.getEntryPoints()); + List combinedEntryPoints = new java.util.ArrayList<>(this.metadata.getEntryPoints()); + if (newMetadata.getEntryPoints() != null) combinedEntryPoints.addAll(newMetadata.getEntryPoints()); + + List combinedCallChains = new java.util.ArrayList<>(this.metadata.getCallChains()); + if (newMetadata.getCallChains() != null) combinedCallChains.addAll(newMetadata.getCallChains()); java.util.Map combinedProperties = new java.util.HashMap<>(this.metadata.getProperties()); - combinedProperties.putAll(newMetadata.getProperties()); + if (newMetadata.getProperties() != null) combinedProperties.putAll(newMetadata.getProperties()); this.metadata = CodebaseMetadata.builder() .triggers(Collections.unmodifiableList(combinedTriggers)) .entryPoints(Collections.unmodifiableList(combinedEntryPoints)) + .callChains(Collections.unmodifiableList(combinedCallChains)) .properties(Collections.unmodifiableMap(combinedProperties)) .build(); } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CallChain.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CallChain.java new file mode 100644 index 0000000..0f3fbed --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CallChain.java @@ -0,0 +1,18 @@ +package click.kamil.springstatemachineexporter.analysis.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +@Data +@Builder +@Jacksonized +@JsonIgnoreProperties(ignoreUnknown = true) +public class CallChain { + private final EntryPoint entryPoint; + private final List methodChain; // e.g., ["Controller.submit", "Service.process", "Service.send"] + private final TriggerPoint triggerPoint; +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CodebaseMetadata.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CodebaseMetadata.java index 08bcabf..a0726a5 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CodebaseMetadata.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CodebaseMetadata.java @@ -1,7 +1,9 @@ package click.kamil.springstatemachineexporter.analysis.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Builder; import lombok.Getter; +import lombok.extern.jackson.Jacksonized; import java.util.ArrayList; import java.util.Collections; @@ -10,19 +12,19 @@ import java.util.Map; @Getter @Builder(toBuilder = true) +@Jacksonized +@JsonIgnoreProperties(ignoreUnknown = true) public class CodebaseMetadata { - private final List triggers; - private final List entryPoints; - private final Map properties; + @Builder.Default + private final List triggers = Collections.emptyList(); + @Builder.Default + private final List entryPoints = Collections.emptyList(); + @Builder.Default + private final List callChains = Collections.emptyList(); + @Builder.Default + private final Map properties = Collections.emptyMap(); public static CodebaseMetadata empty() { - return CodebaseMetadata.builder() - .triggers(Collections.emptyList()) - .entryPoints(Collections.emptyList()) - .properties(Collections.emptyMap()) - .build(); + return CodebaseMetadata.builder().build(); } - - public record TriggerPoint(String className, String methodName, String event, Map metadata) {} - public record EntryPoint(String className, String methodName, String type, Map metadata) {} } 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 new file mode 100644 index 0000000..29ca462 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/EntryPoint.java @@ -0,0 +1,22 @@ +package click.kamil.springstatemachineexporter.analysis.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; + +import java.util.Map; + +@Data +@Builder +@Jacksonized +@JsonIgnoreProperties(ignoreUnknown = true) +public class EntryPoint { + public enum Type { REST, KAFKA, RABBIT, JMS, CUSTOM } + + 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 +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/LibraryHint.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/LibraryHint.java new file mode 100644 index 0000000..34b787a --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/LibraryHint.java @@ -0,0 +1,17 @@ +package click.kamil.springstatemachineexporter.analysis.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +@Data +@Builder +@Jacksonized +@JsonIgnoreProperties(ignoreUnknown = true) +public class LibraryHint { + private final String methodFqn; // e.g., "com.thirdparty.Workflow.send" + private final String event; // The event it triggers +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java new file mode 100644 index 0000000..0e6f726 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java @@ -0,0 +1,21 @@ +package click.kamil.springstatemachineexporter.analysis.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; + +import java.util.Map; + +@Data +@Builder +@Jacksonized +@JsonIgnoreProperties(ignoreUnknown = true) +public class TriggerPoint { + private final String event; + private final String className; + private final String methodName; + private final String sourceModule; + private final String stateMachineId; // Optional: to link to a specific SM instance + private final int lineNumber; +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java new file mode 100644 index 0000000..8adbe48 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java @@ -0,0 +1,92 @@ +package click.kamil.springstatemachineexporter.analysis.resolver; + +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jdt.core.dom.*; + +@Slf4j +public class ConstantResolver { + + public String resolve(Expression expr, CodebaseContext context) { + if (expr == null) return null; + + // 1. Literal? + if (expr instanceof StringLiteral sl) { + return sl.getLiteralValue(); + } + if (expr instanceof NumberLiteral nl) { + return nl.getToken(); + } + if (expr instanceof BooleanLiteral bl) { + return String.valueOf(bl.booleanValue()); + } + + // 2. JDT Bindings? (Most robust) + IVariableBinding binding = null; + if (expr instanceof SimpleName sn) { + IBinding b = sn.resolveBinding(); + if (b instanceof IVariableBinding vb) binding = vb; + } else if (expr instanceof QualifiedName qn) { + IBinding b = qn.resolveBinding(); + if (b instanceof IVariableBinding vb) binding = vb; + } + + if (binding != null && binding.isField()) { + Object value = binding.getConstantValue(); + if (value != null) return value.toString(); + } + + // 3. Fallback: Manual AST Traversal (if bindings not available or resolved) + if (expr instanceof InfixExpression infix) { + return resolveInfix(infix, context); + } + if (expr instanceof QualifiedName qn) { + return resolveManual(qn, context); + } + + return null; + } + + private String resolveInfix(InfixExpression expr, CodebaseContext context) { + if (expr.getOperator() != InfixExpression.Operator.PLUS) return null; + + String left = resolve(expr.getLeftOperand(), context); + String right = resolve(expr.getRightOperand(), context); + + if (left == null || right == null) return null; + + StringBuilder sb = new StringBuilder(left + right); + if (expr.hasExtendedOperands()) { + for (Object operand : expr.extendedOperands()) { + String val = resolve((Expression) operand, context); + if (val == null) return null; + sb.append(val); + } + } + return sb.toString(); + } + + private String resolveManual(QualifiedName qn, CodebaseContext context) { + String qualifier = qn.getQualifier().toString(); + String name = qn.getName().getIdentifier(); + + CompilationUnit contextCu = (CompilationUnit) qn.getRoot(); + TypeDeclaration td = context.getTypeDeclaration(qualifier, contextCu); + + if (td != null) { + for (FieldDeclaration fd : td.getFields()) { + for (Object fragmentObj : fd.fragments()) { + if (fragmentObj instanceof VariableDeclarationFragment fragment) { + if (fragment.getName().getIdentifier().equals(name)) { + Expression initializer = fragment.getInitializer(); + // Simple recursive resolve (be careful of cycles) + if (initializer instanceof StringLiteral sl) return sl.getLiteralValue(); + } + } + } + } + } + + return null; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PropertyResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PropertyResolver.java new file mode 100644 index 0000000..992bcb3 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PropertyResolver.java @@ -0,0 +1,95 @@ +package click.kamil.springstatemachineexporter.analysis.resolver; + +import lombok.extern.slf4j.Slf4j; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Stream; + +@Slf4j +public class PropertyResolver { + + private final List activeProfiles = new ArrayList<>(); + + public void setActiveProfiles(List profiles) { + this.activeProfiles.clear(); + if (profiles != null) { + this.activeProfiles.addAll(profiles); + } + } + + public Map resolveProperties(Path rootDir) { + Map allProperties = new HashMap<>(); + + // 1. Load base properties + loadMatching(rootDir, "application.properties", allProperties); + loadMatching(rootDir, "application.yml", allProperties); + loadMatching(rootDir, "application.yaml", allProperties); + + // 2. Load profile-specific properties (overriding base) + for (String profile : activeProfiles) { + loadMatching(rootDir, "application-" + profile + ".properties", allProperties); + loadMatching(rootDir, "application-" + profile + ".yml", allProperties); + loadMatching(rootDir, "application-" + profile + ".yaml", allProperties); + } + + return allProperties; + } + + private void loadMatching(Path rootDir, String fileName, Map target) { + try (Stream paths = Files.walk(rootDir)) { + paths.filter(p -> Files.isRegularFile(p)) + .filter(p -> p.getFileName().toString().equals(fileName)) + .forEach(p -> { + if (p.toString().endsWith(".properties")) { + target.putAll(loadProperties(p)); + } else { + target.putAll(loadYaml(p)); + } + }); + } catch (IOException e) { + log.error("Failed to scan for {} in {}", fileName, rootDir, e); + } + } + + private Map loadProperties(Path path) { + Map props = new HashMap<>(); + Properties p = new Properties(); + try (InputStream is = Files.newInputStream(path)) { + p.load(is); + p.forEach((k, v) -> props.put(k.toString(), v.toString())); + } catch (IOException e) { + log.warn("Failed to load properties from {}", path); + } + return props; + } + + private Map loadYaml(Path path) { + // Placeholder for future YAML support + log.warn("YAML parsing not fully implemented yet for {}", path); + return new HashMap<>(); + } + + public String resolveValue(String placeholder, Map properties) { + if (placeholder == null || !placeholder.contains("${")) return placeholder; + + // Very basic placeholder extraction: ${key} or ${key:default} + String content = placeholder.substring(placeholder.indexOf("${") + 2, placeholder.lastIndexOf("}")); + String key = content; + String defaultValue = null; + + if (content.contains(":")) { + int colonIndex = content.indexOf(":"); + key = content.substring(0, colonIndex); + defaultValue = content.substring(colonIndex + 1); + } + + return properties.getOrDefault(key, defaultValue); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java new file mode 100644 index 0000000..15cb7b7 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java @@ -0,0 +1,159 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.CallChain; +import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jdt.core.dom.*; + +import java.util.*; + +@Slf4j +@RequiredArgsConstructor +public class CallGraphBuilder { + private final CodebaseContext context; + + public List findChains(List entryPoints, List triggers) { + Map> callGraph = buildCallGraph(); + List chains = new ArrayList<>(); + + for (EntryPoint ep : entryPoints) { + String startMethod = ep.getClassName() + "." + ep.getMethodName(); + for (TriggerPoint tp : triggers) { + String targetMethod = tp.getClassName() + "." + tp.getMethodName(); + List path = findPath(startMethod, targetMethod, callGraph, new HashSet<>()); + if (path != null) { + chains.add(CallChain.builder() + .entryPoint(ep) + .triggerPoint(tp) + .methodChain(path) + .build()); + } + } + } + return chains; + } + + private Map> buildCallGraph() { + Map> graph = new HashMap<>(); + for (CompilationUnit cu : context.getCompilationUnits()) { + cu.accept(new ASTVisitor() { + private String currentMethodFqn; + + @Override + public boolean visit(MethodDeclaration node) { + TypeDeclaration td = findEnclosingType(node); + if (td != null) { + currentMethodFqn = context.getFqn(td) + "." + node.getName().getIdentifier(); + } + return super.visit(node); + } + + @Override + public boolean visit(MethodInvocation node) { + if (currentMethodFqn != null) { + List calledMethods = resolveCalledMethodsPolymorphic(node); + for (String calledMethod : calledMethods) { + graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(calledMethod); + } + } + return super.visit(node); + } + }); + } + return graph; + } + + private List resolveCalledMethodsPolymorphic(MethodInvocation node) { + String baseCalled = resolveCalledMethod(node); + if (baseCalled == null) return Collections.emptyList(); + + List allResolved = new ArrayList<>(); + allResolved.add(baseCalled); + + // Polymorphic fan-out + if (baseCalled.contains(".")) { + String className = baseCalled.substring(0, baseCalled.lastIndexOf('.')); + String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1); + + List impls = context.getImplementations(className); + for (String impl : impls) { + allResolved.add(impl + "." + methodName); + } + } + + return allResolved; + } + + private String resolveCalledMethod(MethodInvocation node) { + Expression receiver = node.getExpression(); + String methodName = node.getName().getIdentifier(); + + if (receiver == null) { + // Local call or static import (simplified) + TypeDeclaration td = findEnclosingType(node); + return td != null ? context.getFqn(td) + "." + methodName : null; + } + + // Try to resolve receiver type + // This is a simplified resolution without full JDT bindings. + // It works for local variables and fields within the same project. + ITypeBinding binding = receiver.resolveTypeBinding(); + if (binding != null) { + return binding.getQualifiedName() + "." + methodName; + } + + // Fallback: heuristic resolution for simple cases like 'orderService.process()' + if (receiver instanceof SimpleName sn) { + String receiverName = sn.getIdentifier(); + // Look for fields or variables (simplified) + // For now, we'll return a "best guess" if we can't resolve it perfectly + // In a real implementation, we'd use JDT bindings more heavily. + return receiverName + "." + methodName; + } + + return null; + } + + private List findPath(String start, String target, Map> graph, Set visited) { + if (start.equals(target)) return new ArrayList<>(List.of(start)); + if (!visited.add(start)) return null; + + List neighbors = graph.get(start); + if (neighbors != null) { + for (String neighbor : neighbors) { + // Heuristic: neighbor might be 'orderService.process' while target is 'click.kamil...OrderService.process' + if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) { + return new ArrayList<>(List.of(start, target)); + } + List path = findPath(neighbor, target, graph, visited); + if (path != null) { + path.add(0, start); + return path; + } + } + } + return null; + } + + private boolean isHeuristicMatch(String neighbor, String target) { + // Match 'orderService.process' with 'click.kamil.OrderService.process' + if (target.endsWith("." + neighbor)) return true; + // Match simple names if we don't have FQN + String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target; + String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor; + + // Very basic heuristic for now + return neighbor.toLowerCase().contains(simpleTarget.toLowerCase()) || target.toLowerCase().contains(simpleNeighbor.toLowerCase()); + } + + private TypeDeclaration findEnclosingType(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof TypeDeclaration)) { + parent = parent.getParent(); + } + return (TypeDeclaration) parent; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CodebaseIntelligenceProvider.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CodebaseIntelligenceProvider.java new file mode 100644 index 0000000..8055a73 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CodebaseIntelligenceProvider.java @@ -0,0 +1,30 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.CallChain; +import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; + +import java.util.List; +import java.util.Map; + +public interface CodebaseIntelligenceProvider { + /** + * Discovers all locations where events are triggered. + */ + List findTriggerPoints(); + + /** + * Discovers all entry points (REST, Kafka, etc.). + */ + List findEntryPoints(); + + /** + * Links Entry Points to Trigger Points. + */ + List findCallChains(List entryPoints, List triggers); + + /** + * Resolves configuration properties. + */ + Map resolveProperties(); +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EnrichmentService.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EnrichmentService.java index 222feb8..bb8a4b5 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EnrichmentService.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EnrichmentService.java @@ -11,9 +11,9 @@ import java.util.List; public class EnrichmentService { private final List enrichers; - public void enrich(AnalysisResult result, CodebaseContext context) { + public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) { for (AnalysisEnricher enricher : enrichers) { - enricher.enrich(result, context); + enricher.enrich(result, context, intelligence); } } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java new file mode 100644 index 0000000..6226f94 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java @@ -0,0 +1,147 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.LibraryHint; +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jdt.core.dom.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +public class GenericEventDetector { + private final CodebaseContext context; + private final ConstantResolver constantResolver; + private final List hints; + + public List detect(CompilationUnit cu) { + List triggers = new ArrayList<>(); + String fileName = cu.getJavaElement() != null ? cu.getJavaElement().getElementName() : "unknown"; + log.debug("Scanning for triggers in {}", fileName); + + cu.accept(new ASTVisitor() { + @Override + public boolean visit(MethodInvocation node) { + String methodName = node.getName().getIdentifier(); + + // 1. Core sendEvent + if ("sendEvent".equals(methodName)) { + processSendEvent(node, cu, triggers); + } + + // 2. Library Hints + processHints(node, cu, triggers); + + return super.visit(node); + } + }); + return triggers; + } + + private void processSendEvent(MethodInvocation node, CompilationUnit cu, List triggers) { + log.info("Found potential trigger: {}", node); + TriggerPoint trigger = buildTriggerPoint(node, cu, null); + if (trigger != null) { + log.info("Successfully built trigger point: {}", trigger.getEvent()); + triggers.add(trigger); + } + } + + private void processHints(MethodInvocation node, CompilationUnit cu, List triggers) { + if (hints == null || hints.isEmpty()) return; + + String calledMethod = resolveCalledMethodName(node); + if (calledMethod == null) return; + + log.info("Checking method call '{}' against {} hints", calledMethod, hints.size()); + + for (LibraryHint hint : hints) { + if (calledMethod.equals(hint.getMethodFqn()) || isHintMatch(calledMethod, hint.getMethodFqn())) { + log.info("MATCH! Found hint for '{}'", calledMethod); + TriggerPoint trigger = buildTriggerPoint(node, cu, hint.getEvent()); + if (trigger != null) { + log.info("Successfully built synthetic trigger point from hint: {}", trigger.getEvent()); + triggers.add(trigger); + } + } + } + } + + private boolean isHintMatch(String called, String hintFqn) { + return hintFqn.endsWith("." + called); + } + + private String resolveCalledMethodName(MethodInvocation node) { + Expression receiver = node.getExpression(); + String methodName = node.getName().getIdentifier(); + if (receiver == null) return methodName; + + if (receiver instanceof SimpleName sn) { + String name = sn.getIdentifier(); + // Check if it's a field in the enclosing class + TypeDeclaration td = findEnclosingType(node); + if (td != null) { + for (FieldDeclaration fd : td.getFields()) { + for (Object fragObj : fd.fragments()) { + if (fragObj instanceof VariableDeclarationFragment fragment) { + if (fragment.getName().getIdentifier().equals(name)) { + String typeName = fd.getType().toString(); + return typeName + "." + methodName; + } + } + } + } + } + return name + "." + methodName; + } + + return receiver.toString() + "." + methodName; + } + + private TriggerPoint buildTriggerPoint(MethodInvocation node, CompilationUnit cu, String forcedEvent) { + String eventValue; + if (forcedEvent != null) { + eventValue = forcedEvent; + } else { + if (node.arguments().isEmpty()) return null; + Expression eventExpr = (Expression) node.arguments().get(0); + eventValue = constantResolver.resolve(eventExpr, context); + if (eventValue == null) { + eventValue = eventExpr.toString(); + } + } + + MethodDeclaration method = findEnclosingMethod(node); + TypeDeclaration type = findEnclosingType(node); + + if (type == null) return null; + + return TriggerPoint.builder() + .event(eventValue) + .className(context.getFqn(type)) + .methodName(method != null ? method.getName().getIdentifier() : "initializer") + .lineNumber(cu.getLineNumber(node.getStartPosition())) + .build(); + } + + private MethodDeclaration findEnclosingMethod(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof MethodDeclaration)) { + parent = parent.getParent(); + } + return (MethodDeclaration) parent; + } + + private TypeDeclaration findEnclosingType(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof TypeDeclaration)) { + parent = parent.getParent(); + } + return (TypeDeclaration) parent; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/InterceptorDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/InterceptorDetector.java new file mode 100644 index 0000000..52172ef --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/InterceptorDetector.java @@ -0,0 +1,75 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; +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 InterceptorDetector { + private final CodebaseContext context; + + public List detect(CompilationUnit cu) { + List entryPoints = new ArrayList<>(); + cu.accept(new ASTVisitor() { + @Override + public boolean visit(TypeDeclaration node) { + if (isInterceptor(node)) { + processInterceptor(node, entryPoints); + } + return super.visit(node); + } + }); + return entryPoints; + } + + private boolean isInterceptor(TypeDeclaration node) { + if (node.getSuperclassType() != null) { + // Check for common adapter/base classes if needed + } + for (Object interfaceType : node.superInterfaceTypes()) { + String name = interfaceType.toString(); + if (name.contains("HandlerInterceptor") || name.contains("Filter") || name.contains("WebRequestInterceptor")) { + return true; + } + } + return false; + } + + private void processInterceptor(TypeDeclaration node, List entryPoints) { + String fqn = context.getFqn(node); + for (MethodDeclaration method : node.getMethods()) { + String methodName = method.getName().getIdentifier(); + if (isInterceptorMethod(methodName)) { + Map metadata = new HashMap<>(); + metadata.put("interceptorType", getInterceptorType(node)); + + entryPoints.add(EntryPoint.builder() + .type(EntryPoint.Type.CUSTOM) + .name("INTERCEPTOR: " + node.getName().getIdentifier() + "." + methodName) + .className(fqn) + .methodName(methodName) + .metadata(metadata) + .build()); + } + } + } + + private boolean isInterceptorMethod(String name) { + return name.equals("preHandle") || name.equals("postHandle") || name.equals("afterCompletion") || name.equals("doFilter"); + } + + private String getInterceptorType(TypeDeclaration node) { + for (Object interfaceType : node.superInterfaceTypes()) { + String name = interfaceType.toString(); + if (name.contains("HandlerInterceptor")) return "Spring MVC Interceptor"; + if (name.contains("Filter")) return "Servlet Filter"; + } + return "Generic Interceptor"; + } +} 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 new file mode 100644 index 0000000..5ede38b --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtIntelligenceProvider.java @@ -0,0 +1,74 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.CallChain; +import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.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.List; +import java.util.Map; + +@Slf4j +public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider { + private final CodebaseContext context; + private final Path rootDir; + private final ConstantResolver constantResolver = new ConstantResolver(); + private final GenericEventDetector eventDetector; + private final SpringMvcDetector mvcDetector; + private final CallGraphBuilder callGraphBuilder; + private final LifecycleDetector lifecycleDetector; + private final InterceptorDetector interceptorDetector; + + public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) { + this.context = context; + this.rootDir = rootDir; + this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints()); + this.mvcDetector = new SpringMvcDetector(context, constantResolver); + this.callGraphBuilder = new CallGraphBuilder(context); + this.lifecycleDetector = new LifecycleDetector(context); + this.interceptorDetector = new InterceptorDetector(context); + } + + @Override + public List findTriggerPoints() { + List allTriggers = new java.util.ArrayList<>(); + var cus = context.getCompilationUnits(); + log.info("JdtIntelligenceProvider scanning {} compilation units for triggers", cus.size()); + for (CompilationUnit cu : cus) { + allTriggers.addAll(eventDetector.detect(cu)); + allTriggers.addAll(lifecycleDetector.detect(cu)); + } + log.info("Found {} triggers (including lifecycle) in total", allTriggers.size()); + return allTriggers; + } + + @Override + public List findEntryPoints() { + List allEntryPoints = new java.util.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(interceptorDetector.detect(cu)); + } + log.info("Found {} entry points (including interceptors) in total", allEntryPoints.size()); + return allEntryPoints; + } + + @Override + public List findCallChains(List entryPoints, List triggers) { + log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers", entryPoints.size(), triggers.size()); + return callGraphBuilder.findChains(entryPoints, triggers); + } + + @Override + public Map resolveProperties() { + return context.getProperties(); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/LifecycleDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/LifecycleDetector.java new file mode 100644 index 0000000..778470b --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/LifecycleDetector.java @@ -0,0 +1,62 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.RequiredArgsConstructor; +import org.eclipse.jdt.core.dom.*; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +public class LifecycleDetector { + private final CodebaseContext context; + + public List detect(CompilationUnit cu) { + List lifecyclePoints = new ArrayList<>(); + cu.accept(new ASTVisitor() { + @Override + public boolean visit(MethodInvocation node) { + String methodName = node.getName().getIdentifier(); + if ("restore".equals(methodName) || "reset".equals(methodName)) { + TriggerPoint point = buildLifecyclePoint(node, cu, methodName); + if (point != null) { + lifecyclePoints.add(point); + } + } + return super.visit(node); + } + }); + return lifecyclePoints; + } + + private TriggerPoint buildLifecyclePoint(MethodInvocation node, CompilationUnit cu, String type) { + MethodDeclaration method = findEnclosingMethod(node); + TypeDeclaration typeDecl = findEnclosingType(node); + + if (typeDecl == null) return null; + + return TriggerPoint.builder() + .event("[LIFECYCLE:" + type.toUpperCase() + "]") + .className(context.getFqn(typeDecl)) + .methodName(method != null ? method.getName().getIdentifier() : "initializer") + .lineNumber(cu.getLineNumber(node.getStartPosition())) + .build(); + } + + private MethodDeclaration findEnclosingMethod(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof MethodDeclaration)) { + parent = parent.getParent(); + } + return (MethodDeclaration) parent; + } + + private TypeDeclaration findEnclosingType(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof TypeDeclaration)) { + parent = parent.getParent(); + } + return (TypeDeclaration) parent; + } +} 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 new file mode 100644 index 0000000..86f6452 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringMvcDetector.java @@ -0,0 +1,179 @@ +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 lombok.extern.slf4j.Slf4j; +import org.eclipse.jdt.core.dom.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@RequiredArgsConstructor +public class SpringMvcDetector { + private final CodebaseContext context; + private final ConstantResolver constantResolver; + + public List detect(CompilationUnit cu) { + List entryPoints = new ArrayList<>(); + cu.accept(new ASTVisitor() { + @Override + public boolean visit(TypeDeclaration node) { + if (isRestController(node)) { + processController(node, entryPoints); + } + return super.visit(node); + } + }); + return entryPoints; + } + + private boolean isRestController(TypeDeclaration node) { + for (Object modifier : node.modifiers()) { + if (modifier instanceof Annotation annotation) { + String name = annotation.getTypeName().getFullyQualifiedName(); + if (name.endsWith("RestController") || name.endsWith("Controller")) { + return true; + } + } + } + return false; + } + + private void processController(TypeDeclaration controller, List entryPoints) { + String basePath = getMappingPathHierarchical(controller); + for (MethodDeclaration method : controller.getMethods()) { + EntryPoint ep = processMethodHierarchical(method, basePath, controller); + if (ep != null) { + entryPoints.add(ep); + } + } + } + + 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) { + path = getMappingPath(itd); + if (!path.isEmpty()) return path; + } + } + + // 3. Check superclass + if (td.getSuperclassType() != null) { + TypeDeclaration std = context.getTypeDeclaration(td.getSuperclassType().toString()); + if (std != null) { + return getMappingPathHierarchical(std); + } + } + + return ""; + } + + 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()); + if (itd != null) { + for (MethodDeclaration im : itd.getMethods()) { + if (im.getName().getIdentifier().equals(methodName)) { + ep = processMethod(im, basePath, controller); + if (ep != null) return ep; + } + } + } + } + + return null; + } + + private EntryPoint processMethod(MethodDeclaration method, String basePath, TypeDeclaration controller) { + for (Object modifier : method.modifiers()) { + if (modifier instanceof Annotation annotation) { + String name = annotation.getTypeName().getFullyQualifiedName(); + String verb = getHttpVerb(name); + if (verb != null) { + String methodPath = getMappingPath(annotation); + String fullPath = combinePaths(basePath, methodPath); + + Map metadata = new HashMap<>(); + metadata.put("path", fullPath); + metadata.put("verb", verb); + + return EntryPoint.builder() + .type(EntryPoint.Type.REST) + .name(verb + " " + fullPath) + .className(context.getFqn(controller)) + .methodName(method.getName().getIdentifier()) + .metadata(metadata) + .build(); + } + } + } + return null; + } + + 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("RequestMapping")) return "REQUEST"; + return null; + } + + private String getMappingPath(TypeDeclaration td) { + for (Object modifier : td.modifiers()) { + if (modifier instanceof Annotation annotation) { + if (annotation.getTypeName().getFullyQualifiedName().endsWith("RequestMapping")) { + return getMappingPath(annotation); + } + } + } + return ""; + } + + private String getMappingPath(Annotation annotation) { + 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 ("value".equals(pair.getName().getIdentifier()) || "path".equals(pair.getName().getIdentifier())) { + value = pair.getValue(); + break; + } + } + } + + if (value != null) { + String resolved = constantResolver.resolve(value, context); + return resolved != null ? resolved : value.toString(); + } + return ""; + } + + private String combinePaths(String base, String method) { + String b = base.startsWith("/") ? base : "/" + base; + if (b.endsWith("/")) b = b.substring(0, b.length() - 1); + String m = method.startsWith("/") ? method : "/" + method; + if ("/".equals(m)) m = ""; + return b + m; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java index 21c8546..a1a459a 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java @@ -1,5 +1,6 @@ package click.kamil.springstatemachineexporter.ast.app; +import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver; import click.kamil.springstatemachineexporter.model.Action; import click.kamil.springstatemachineexporter.model.Guard; import click.kamil.springstatemachineexporter.model.State; @@ -26,6 +27,7 @@ import java.util.Set; import java.util.stream.Collectors; public class AstTransitionParser { + private static final ConstantResolver constantResolver = new ConstantResolver(); private static final Set SUPPORTED_TRANSITION_METHOD_NAMES = Arrays.stream(TransitionType.values()) .map(TransitionType::getMethodName) @@ -215,7 +217,12 @@ public class AstTransitionParser { }); case "event" -> { Expression resolved = resolveArg((Expression) args.get(0), argsMap); - t.setEvent(QuotedExpression.of(resolved).toStringWithoutQuotes()); + String eventValue = constantResolver.resolve(resolved, context); + if (eventValue != null) { + t.setEvent(eventValue); + } else { + t.setEvent(QuotedExpression.of(resolved).toStringWithoutQuotes()); + } } case "guard" -> { Expression resolved = resolveArg((Expression) args.get(0), argsMap); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java index 5508593..37dcbf6 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java @@ -1,24 +1,19 @@ package click.kamil.springstatemachineexporter.ast.common; +import click.kamil.springstatemachineexporter.analysis.model.LibraryHint; +import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver; +import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver; import click.kamil.springstatemachineexporter.model.State; -import org.eclipse.jdt.core.dom.AST; -import org.eclipse.jdt.core.dom.ASTParser; -import org.eclipse.jdt.core.dom.Annotation; -import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.dom.Expression; -import org.eclipse.jdt.core.dom.ImportDeclaration; -import org.eclipse.jdt.core.dom.MethodDeclaration; -import org.eclipse.jdt.core.dom.Modifier; -import org.eclipse.jdt.core.dom.QualifiedName; -import org.eclipse.jdt.core.dom.SimpleName; -import org.eclipse.jdt.core.dom.StringLiteral; -import org.eclipse.jdt.core.dom.Type; -import org.eclipse.jdt.core.dom.TypeDeclaration; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.jdt.core.dom.*; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -30,36 +25,135 @@ public class CodebaseContext { private final Map classPaths = new HashMap<>(); private final Map simpleNameToFqn = new HashMap<>(); private final Set ambiguousSimpleNames = new HashSet<>(); + private final ConstantResolver constantResolver = new ConstantResolver(); + private final PropertyResolver propertyResolver = new PropertyResolver(); + private Map properties = new HashMap<>(); + private List libraryHints = new ArrayList<>(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + public void loadLibraryHints(Path hintsFile) throws IOException { + if (Files.exists(hintsFile)) { + System.out.println("Loading hints from " + hintsFile.toAbsolutePath()); + this.libraryHints = objectMapper.readValue(hintsFile.toFile(), new TypeReference>() {}); + System.out.println("Loaded " + libraryHints.size() + " library hints"); + } + } + + public List getLibraryHints() { + return Collections.unmodifiableList(libraryHints); + } + + private final Set ignorePatterns = new HashSet<>(Set.of("**/test/**", "**/build/**", "**/node_modules/**", "**/out/**")); + private String[] classpath = new String[0]; + private String[] sourcepath = new String[0]; + private boolean resolveBindings = false; + + public Map getProperties() { + return Collections.unmodifiableMap(properties); + } + + public void setClasspath(List classpath) { + this.classpath = classpath.toArray(new String[0]); + } + + public void setSourcepath(List sourcepath) { + this.sourcepath = sourcepath.toArray(new String[0]); + } + + public void setResolveBindings(boolean resolveBindings) { + this.resolveBindings = resolveBindings; + } + + public void setActiveProfiles(List profiles) { + this.propertyResolver.setActiveProfiles(profiles); + } public void scan(Path rootDir) throws IOException { - List javaFiles = FileUtils.findFilesWithExtension(Set.of(rootDir), ".java"); + scan(rootDir, Collections.emptySet()); + } + + private final List allCus = new ArrayList<>(); + + private final Map> interfaceToImpls = new HashMap<>(); + + public void scan(Path rootDir, Set customIgnorePatterns) throws IOException { + this.properties = propertyResolver.resolveProperties(rootDir); + + Set activeIgnore = new HashSet<>(ignorePatterns); + activeIgnore.addAll(customIgnorePatterns); + + List javaFiles = FileUtils.findFilesWithExtension(Set.of(rootDir), ".java", activeIgnore); for (Path javaFile : javaFiles) { String source = Files.readString(javaFile); - CompilationUnit cu = parse(source); + CompilationUnit cu = parse(source, javaFile.getFileName().toString()); + allCus.add(cu); + String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : ""; for (Object type : cu.types()) { if (type instanceof TypeDeclaration td) { - String simpleName = td.getName().getIdentifier(); - String fqn = packageName.isEmpty() ? simpleName : packageName + "." + simpleName; - classes.put(fqn, cu); - classPaths.put(fqn, javaFile); - - if (simpleNameToFqn.containsKey(simpleName)) { - String existingFqn = simpleNameToFqn.get(simpleName); - if (!existingFqn.equals(fqn)) { - ambiguousSimpleNames.add(simpleName); - } - } else { - simpleNameToFqn.put(simpleName, fqn); - } + indexType(td, packageName, javaFile); } } } } + private void indexType(TypeDeclaration td, String packageName, Path javaFile) { + String simpleName = td.getName().getIdentifier(); + String fqn = packageName.isEmpty() ? simpleName : packageName + "." + simpleName; + + classes.put(fqn, (CompilationUnit) td.getRoot()); + classPaths.put(fqn, javaFile); + + if (simpleNameToFqn.containsKey(simpleName)) { + String existingFqn = simpleNameToFqn.get(simpleName); + if (!existingFqn.equals(fqn)) { + ambiguousSimpleNames.add(simpleName); + } + } else { + simpleNameToFqn.put(simpleName, fqn); + } + + // Inheritance Mapping + for (Object itf : td.superInterfaceTypes()) { + String itfName = itf.toString(); + interfaceToImpls.computeIfAbsent(itfName, k -> new ArrayList<>()).add(fqn); + } + } + + public List getImplementations(String interfaceName) { + // Try direct match + List impls = interfaceToImpls.get(interfaceName); + if (impls != null) return impls; + + // Try FQN match if input was simple name + String fqn = simpleNameToFqn.get(interfaceName); + if (fqn != null) return interfaceToImpls.getOrDefault(fqn, Collections.emptyList()); + + return Collections.emptyList(); + } + public State resolveState(Expression expr, CompilationUnit cu) { if (expr == null) return null; + + // 1. Check for constants + String resolvedValue = constantResolver.resolve(expr, this); + if (resolvedValue != null) { + return State.of(expr.toString(), resolvedValue); + } + + // 2. Check for @Value fields (SimpleName) + if (expr instanceof SimpleName sn) { + String fieldName = sn.getIdentifier(); + TypeDeclaration td = findEnclosingType(sn); + if (td != null) { + String value = findValueFromField(td, fieldName); + if (value != null) { + return State.of(fieldName, value); + } + } + } + if (expr instanceof StringLiteral sl) return State.of(sl.getLiteralValue()); @@ -75,6 +169,55 @@ public class CodebaseContext { return State.of(raw); } + private String findValueFromField(TypeDeclaration td, String fieldName) { + for (FieldDeclaration fd : td.getFields()) { + for (Object fragObj : fd.fragments()) { + if (fragObj instanceof VariableDeclarationFragment fragment) { + if (fragment.getName().getIdentifier().equals(fieldName)) { + // Check for @Value annotation + for (Object mod : fd.modifiers()) { + if (mod instanceof Annotation ann) { + String name = ann.getTypeName().getFullyQualifiedName(); + if (name.endsWith("Value")) { + String placeholder = extractAnnotationValue(ann); + return propertyResolver.resolveValue(placeholder, properties); + } + } + } + } + } + } + } + return null; + } + + private String extractAnnotationValue(Annotation ann) { + if (ann instanceof SingleMemberAnnotation sma) { + return stripQuotes(sma.getValue().toString()); + } else if (ann instanceof NormalAnnotation na) { + for (Object pairObj : na.values()) { + MemberValuePair pair = (MemberValuePair) pairObj; + if ("value".equals(pair.getName().getIdentifier())) { + return stripQuotes(pair.getValue().toString()); + } + } + } + return null; + } + + private String stripQuotes(String s) { + if (s == null) return null; + return s.replaceAll("^\"|\"$", ""); + } + + private TypeDeclaration findEnclosingType(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof TypeDeclaration)) { + parent = parent.getParent(); + } + return (TypeDeclaration) parent; + } + private String resolveQualifiedName(QualifiedName qn, CompilationUnit cu) { String qualifier = qn.getQualifier().toString(); String name = qn.getName().getIdentifier(); @@ -97,11 +240,15 @@ public class CodebaseContext { return name; } - private CompilationUnit parse(String source) { + private CompilationUnit parse(String source, String unitName) { ASTParser parser = ASTParser.newParser(AST.getJLSLatest()); parser.setSource(source.toCharArray()); parser.setKind(ASTParser.K_COMPILATION_UNIT); - parser.setResolveBindings(false); + parser.setResolveBindings(resolveBindings); + if (resolveBindings) { + parser.setUnitName(unitName); + parser.setEnvironment(classpath, sourcepath, null, true); + } return (CompilationUnit) parser.createAST(null); } @@ -321,4 +468,8 @@ public class CodebaseContext { private String getSimpleName(Type type) { return AstUtils.extractSimpleTypeName(type); } + + public Collection getCompilationUnits() { + return Collections.unmodifiableCollection(allCus); + } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/FileUtils.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/FileUtils.java index 84e52a9..7a300bc 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/FileUtils.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/FileUtils.java @@ -1,8 +1,11 @@ package click.kamil.springstatemachineexporter.ast.common; import java.io.IOException; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -10,6 +13,14 @@ import java.util.stream.Stream; public class FileUtils { public static List findFilesWithExtension(Set startDirs, String extension) throws RuntimeException { + return findFilesWithExtension(startDirs, extension, Collections.emptySet()); + } + + public static List findFilesWithExtension(Set startDirs, String extension, Set ignorePatterns) throws RuntimeException { + List matchers = ignorePatterns.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + try (Stream paths = startDirs.stream() .flatMap(startDir -> { try { @@ -20,6 +31,7 @@ public class FileUtils { })) { return paths .filter(p -> Files.isRegularFile(p) && p.toString().endsWith(extension)) + .filter(p -> matchers.stream().noneMatch(m -> m.matches(p))) .collect(Collectors.toList()); } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/Dot.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/Dot.java index 7f6d7f6..18d7b9a 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/Dot.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/Dot.java @@ -19,21 +19,20 @@ public class Dot implements StateMachineExporter { @Override public String export(click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result, ExportOptions options) { - return export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options); - } - - @Override - public String export(String name, - List transitions, - Set startStates, - Set endStates, - ExportOptions options) { StringBuilder sb = new StringBuilder(); sb.append("digraph statemachine {\n"); sb.append(" rankdir=LR;\n"); sb.append(" node [shape=rounded, style=filled, fillcolor=white, fontname=\"Arial\"];\n"); sb.append(" edge [fontname=\"Arial\", fontsize=10];\n\n"); + // --- Core State Machine Rendering --- + renderCore(result.getTransitions(), result.getStartStates(), result.getEndStates(), options, sb); + + sb.append("}\n"); + return sb.toString(); + } + + private void renderCore(List transitions, Set startStates, Set endStates, ExportOptions options, StringBuilder sb) { // Choices Set statesWithChoice = new LinkedHashSet<>(); Map choiceColorMap = new HashMap<>(); @@ -187,7 +186,22 @@ public class Dot implements StateMachineExporter { } } } + } + @Override + public String export(String name, + List transitions, + Set startStates, + Set endStates, + ExportOptions options) { + StringBuilder sb = new StringBuilder(); + sb.append("digraph statemachine {\n"); + sb.append(" rankdir=LR;\n"); + sb.append(" node [shape=rounded, style=filled, fillcolor=white, fontname=\"Arial\"];\n"); + sb.append(" edge [fontname=\"Arial\", fontsize=10];\n\n"); + + renderCore(transitions, startStates, endStates, options, sb); + sb.append("}\n"); return sb.toString(); } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/JsonExporter.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/JsonExporter.java index d85b2cc..34a502c 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/JsonExporter.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/JsonExporter.java @@ -19,7 +19,19 @@ public class JsonExporter implements StateMachineExporter { @Override public String export(click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result, ExportOptions options) { - return export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options); + try { + java.util.Map output = new java.util.HashMap<>(); + output.put("name", result.getName()); + output.put("transitions", result.getTransitions()); + output.put("startStates", result.getStartStates()); + output.put("endStates", result.getEndStates()); + output.put("renderChoicesAsDiamonds", options.isRenderChoicesAsDiamonds()); + output.put("metadata", result.getMetadata()); + + return objectMapper.writeValueAsString(output); + } catch (Exception e) { + throw new RuntimeException("Failed to export to JSON", e); + } } @Override diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/PlantUml.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/PlantUml.java index 3761387..40f104c 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/PlantUml.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/PlantUml.java @@ -11,6 +11,7 @@ import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -35,19 +36,7 @@ public class PlantUml implements StateMachineExporter { @Override public String export(click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result, ExportOptions options) { - String baseContent = export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options); - - // Allow custom styling from metadata - String customStyle = result.getMetadata().getProperties().get("plantuml.style"); - if (customStyle != null && !customStyle.isBlank()) { - // Insert custom style before @enduml - int endumlIndex = baseContent.lastIndexOf("@enduml"); - if (endumlIndex >= 0) { - return baseContent.substring(0, endumlIndex) + "\n" + customStyle + "\n" + baseContent.substring(endumlIndex); - } - } - - return baseContent; + return export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options); } @Override @@ -59,6 +48,18 @@ public class PlantUml implements StateMachineExporter { StringBuilder sb = new StringBuilder(); sb.append("@startuml\n"); + sb.append("!pragma layout smetana\n"); + sb.append("hide empty description\n"); + sb.append("hide stereotype\n"); + + // Pre-declare all unique states for layout stability + Set allUniqueStates = new LinkedHashSet<>(); + transitions.forEach(t -> { + t.getSourceStates().forEach(s -> allUniqueStates.add(simplify(s.toString()))); + t.getTargetStates().forEach(s -> allUniqueStates.add(simplify(s.toString()))); + }); + allUniqueStates.forEach(s -> sb.append("state ").append(s).append("\n")); + sb.append("\n"); String style = loadBaseStyle(); @@ -82,7 +83,7 @@ public class PlantUml implements StateMachineExporter { Set statesWithChoice = new java.util.LinkedHashSet<>(); for (Transition t : transitions) { - if (t.getType() == TransitionType.CHOICE) { + if (t.getType() == TransitionType.CHOICE && t.getSourceStates() != null) { for (State source : t.getSourceStates()) { statesWithChoice.add(simplify(source.toString())); } @@ -137,11 +138,10 @@ public class PlantUml implements StateMachineExporter { for (String rawTarget : targets) { String target = simplify(rawTarget); - String transitionSource = source; String styleClass = getStyleClass(t, source, choiceStateClassMap); String color = getLegacyColor(t, source, choiceStateClassMap); - sb.append(transitionSource).append(" -[#").append(color).append(",bold]-> ").append(target).append(" <<").append(styleClass).append(">>"); + sb.append(source).append(" -[#").append(color).append(",bold]-> ").append(target).append(" <<").append(styleClass).append(">>"); if (t.getEvent() != null && !t.getEvent().isBlank()) { String eventClass = "e_" + normalize(t.getEvent()); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java index 1f4538b..0498617 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java @@ -1,7 +1,13 @@ package click.kamil.springstatemachineexporter.service; +import click.kamil.springstatemachineexporter.analysis.enricher.CallChainEnricher; +import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnricher; +import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher; +import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher; import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; +import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider; import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService; +import click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider; import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser; import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator; import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils; @@ -10,7 +16,6 @@ import click.kamil.springstatemachineexporter.model.Transition; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import click.kamil.springstatemachineexporter.exporter.ExportOptions; import click.kamil.springstatemachineexporter.exporter.StateMachineExporter; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.TypeDeclaration; @@ -25,36 +30,62 @@ import java.util.List; import java.util.Set; @Slf4j -@RequiredArgsConstructor public class ExportService { private final List exporters; private final EnrichmentService enrichmentService; + + public ExportService(List exporters, EnrichmentService enrichmentService) { + this.exporters = exporters; + this.enrichmentService = enrichmentService; + } + + public ExportService(List exporters) { + this(exporters, new EnrichmentService(List.of( + new TriggerEnricher(), + new EntryPointEnricher(), + new PropertyEnricher(), + new CallChainEnricher() + ))); + } + private static final List TARGET_ANNOTATIONS = List.of("EnableStateMachineFactory", "EnableStateMachine"); public static final Set STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory"); public void runExporter(Path inputDir, Path outputDir, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException { + runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList()); + } + + public void runExporter(Path inputDir, Path outputDir, List selectedFormats, boolean renderChoicesAsDiamonds, List activeProfiles) throws IOException { CodebaseContext context = new CodebaseContext(); + context.setActiveProfiles(activeProfiles); + context.setSourcepath(List.of(inputDir.toString())); + context.setResolveBindings(true); + + Path hintsFile = inputDir.resolve("hints.json"); + if (!Files.exists(hintsFile)) { + hintsFile = inputDir.resolve("src/main/resources/hints.json"); + } + + if (Files.exists(hintsFile)) { + log.info("Loading hints from {}", hintsFile.toAbsolutePath()); + context.loadLibraryHints(hintsFile); + } + context.scan(inputDir); - exportAll(context, outputDir, selectedFormats, renderChoicesAsDiamonds); + + CodebaseIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, inputDir); + exportAll(context, intelligence, outputDir, selectedFormats, renderChoicesAsDiamonds); } public void runJsonExporter(Path jsonFile, Path outputDir, List selectedFormats) throws IOException { JsonImportService jsonImportService = new JsonImportService(); - StateMachineModel model = jsonImportService.importJson(jsonFile); + AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile); - AnalysisResult result = AnalysisResult.builder() - .name(model.getName()) - .transitions(model.getTransitions()) - .startStates(model.getStartStates()) - .endStates(model.getEndStates()) - .renderChoicesAsDiamonds(model.isRenderChoicesAsDiamonds()) - .build(); - generateOutputs(outputDir, result, selectedFormats); } - private void exportAll(CodebaseContext context, Path outputDir, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException { + private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException { Set processedLocations = new HashSet<>(); // 1. Find entry point classes (annotated or extending adapter) @@ -62,7 +93,7 @@ public class ExportService { for (TypeDeclaration td : entryPoints) { String fqn = context.getFqn(td); if (processedLocations.add(fqn)) { - processEntryPoint(td, outputDir, context, selectedFormats, renderChoicesAsDiamonds); + processEntryPoint(td, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds); } } @@ -75,12 +106,12 @@ public class ExportService { String uniqueId = parentFqn + "#" + methodName; if (processedLocations.add(uniqueId)) { - processBeanMethod(m, outputDir, context, selectedFormats, renderChoicesAsDiamonds); + processBeanMethod(m, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds); } } } - private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException { + private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException { String className = context.getFqn(td); log.info("Processing state machine config: {}", className); @@ -105,12 +136,12 @@ public class ExportService { .renderChoicesAsDiamonds(renderChoicesAsDiamonds) .build(); - enrichmentService.enrich(result, context); + enrichmentService.enrich(result, context, intelligence); generateOutputs(outputDir, result, selectedFormats); } - private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException { + private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException { TypeDeclaration parentClass = (TypeDeclaration) m.getParent(); String parentFqn = context.getFqn(parentClass); String methodName = m.getName().getIdentifier(); @@ -131,29 +162,32 @@ public class ExportService { .renderChoicesAsDiamonds(renderChoicesAsDiamonds) .build(); - enrichmentService.enrich(result, context); + enrichmentService.enrich(result, context, intelligence); generateOutputs(outputDir, result, selectedFormats); } private void generateOutputs(Path outputDir, AnalysisResult result, List selectedFormats) throws IOException { + if (selectedFormats == null || selectedFormats.isEmpty()) { + return; + } - Path targetDir = outputDir.resolve(result.getName()); + String actualName = result.getName(); + Path targetDir = outputDir.resolve(actualName); Files.createDirectories(targetDir); ExportOptions options = ExportOptions.builder() - .useLambdaGuards(true) .renderChoicesAsDiamonds(result.isRenderChoicesAsDiamonds()) .build(); - for (StateMachineExporter output : exporters) { - if (selectedFormats != null && !selectedFormats.isEmpty()) { - boolean match = selectedFormats.stream().anyMatch(f -> output.getFileExtension().toLowerCase().contains(f.toLowerCase())); - if (!match) continue; + for (StateMachineExporter exporter : exporters) { + String ext = exporter.getFileExtension().toLowerCase(); + if (!selectedFormats.stream().anyMatch(fmt -> ext.contains(fmt.toLowerCase()))) { + continue; } - String content = output.export(result, options); - String fileName = result.getName() + output.getFileExtension(); + String content = exporter.export(result, options); + String fileName = result.getName() + exporter.getFileExtension(); try (PrintWriter out = new PrintWriter(targetDir.resolve(fileName).toFile())) { out.println(content); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/JsonImportService.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/JsonImportService.java index 6177645..3b62c7a 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/JsonImportService.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/JsonImportService.java @@ -1,5 +1,6 @@ package click.kamil.springstatemachineexporter.service; +import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; import click.kamil.springstatemachineexporter.model.StateMachineModel; import com.fasterxml.jackson.databind.ObjectMapper; @@ -23,4 +24,8 @@ public class JsonImportService { } return model; } + + public AnalysisResult importAnalysisResult(Path jsonFile) throws IOException { + return objectMapper.readValue(jsonFile.toFile(), AnalysisResult.class); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/PlantUmlE2ETest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/PlantUmlE2ETest.java index 13cba55..1f24670 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/PlantUmlE2ETest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/PlantUmlE2ETest.java @@ -28,8 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat; public class PlantUmlE2ETest { private final List exporters = List.of(new PlantUml()); - private final EnrichmentService enrichmentService = new EnrichmentService(Collections.emptyList()); - private final ExportService exportService = new ExportService(exporters, enrichmentService); + private final ExportService exportService = new ExportService(exporters); private static Path findProjectRoot() { Path current = Path.of(".").toAbsolutePath(); @@ -107,6 +106,31 @@ public class PlantUmlE2ETest { root.resolve("state_machines/inheritance_extra_functions3_state_machine"), Path.of("src/test/resources/golden/G2StateMachineConfiguration"), "G2StateMachineConfiguration" + ), + new TestScenario( + "Extended Analysis Sample", + root.resolve("state_machines/extended_analysis_sample"), + Path.of("src/test/resources/golden/ExtendedStateMachineConfig"), + "ExtendedStateMachineConfig" + ), + new TestScenario( + "Extended Analysis Sample (PROD)", + root.resolve("state_machines/extended_analysis_sample"), + Path.of("src/test/resources/golden/ExtendedStateMachineConfig_PROD"), + "ExtendedStateMachineConfig", + List.of("prod") + ), + new TestScenario( + "Inheritance Sample", + root.resolve("state_machines/inheritance_sample"), + Path.of("src/test/resources/golden/InheritanceStateMachineConfig"), + "InheritanceStateMachineConfig" + ), + new TestScenario( + "Enterprise Order System", + root.resolve("state_machines/enterprise_order_system"), + Path.of("src/test/resources/golden/EnterpriseStateMachineConfig"), + "EnterpriseStateMachineConfig" ) ); } @@ -115,7 +139,7 @@ public class PlantUmlE2ETest { @MethodSource("provideTestScenarios") void testPngGenerationMatchesGolden(TestScenario scenario, @TempDir Path tempDir) throws Exception { // 1. Generate PUML - exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml"), true); + exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml"), true, scenario.activeProfiles()); List generatedDirs; try (var stream = Files.list(tempDir)) { @@ -129,34 +153,42 @@ public class PlantUmlE2ETest { .orElseThrow(() -> new IllegalArgumentException("No output directory found for " + targetBaseName)); String actualBaseName = actualDir.getFileName().toString(); - Path pumlFile = actualDir.resolve(actualBaseName + ".puml"); - - assertThat(pumlFile).exists(); - // 2. Render PNG using PlantUML library - String pumlContent = Files.readString(pumlFile); - SourceStringReader reader = new SourceStringReader(pumlContent); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - reader.outputImage(os, new FileFormatOption(FileFormat.PNG)); - byte[] actualPngBytes = os.toByteArray(); + List pumlFiles; + try (var stream = Files.list(actualDir)) { + pumlFiles = stream.filter(p -> p.toString().endsWith(".puml")).toList(); + } - // 3. Compare with Golden - Path goldenPngFile = scenario.goldenPath().resolve(targetBaseName + ".png"); - - if (Boolean.getBoolean("updateGolden")) { - Files.createDirectories(goldenPngFile.getParent()); - Files.write(goldenPngFile, actualPngBytes); - System.out.println("Updated golden PNG: " + goldenPngFile); - } else { - assertThat(goldenPngFile) - .as("Golden PNG file %s should exist", goldenPngFile) - .exists(); + for (Path pumlFile : pumlFiles) { + String fileName = pumlFile.getFileName().toString(); + String baseNoExt = fileName.substring(0, fileName.lastIndexOf('.')); + String goldenBaseNoExt = baseNoExt.replace(actualBaseName, targetBaseName); - byte[] expectedPngBytes = Files.readAllBytes(goldenPngFile); - - assertThat(actualPngBytes) - .as("Visual regression detected for %s. PNG output does not match golden reference.", scenario.name()) - .containsExactly(expectedPngBytes); + // 2. Render PNG using PlantUML library + String pumlContent = Files.readString(pumlFile); + SourceStringReader reader = new SourceStringReader(pumlContent); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + reader.outputImage(os, new FileFormatOption(FileFormat.PNG)); + byte[] actualPngBytes = os.toByteArray(); + + // 3. Compare with Golden + Path goldenPngFile = scenario.goldenPath().resolve(goldenBaseNoExt + ".png"); + + if (System.getProperty("updateGolden", "false").equals("true")) { + Files.createDirectories(scenario.goldenPath()); + Files.write(goldenPngFile, actualPngBytes); + System.out.println("Updated golden PNG: " + goldenPngFile); + } else { + assertThat(goldenPngFile) + .as("Golden PNG missing for %s", fileName) + .exists(); + + byte[] expectedPngBytes = Files.readAllBytes(goldenPngFile); + + assertThat(actualPngBytes) + .as("Visual regression detected for %s (%s). PNG output does not match golden reference.", scenario.name(), fileName) + .containsExactly(expectedPngBytes); + } } } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java index 165804d..4f189f6 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java @@ -22,8 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat; public class RegressionTest { private final List exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter()); - private final EnrichmentService enrichmentService = new EnrichmentService(Collections.emptyList()); - private final ExportService exportService = new ExportService(exporters, enrichmentService); + private final ExportService exportService = new ExportService(exporters); private static Path findProjectRoot() { Path current = Path.of(".").toAbsolutePath(); @@ -101,6 +100,31 @@ public class RegressionTest { root.resolve("state_machines/inheritance_extra_functions3_state_machine"), Path.of("src/test/resources/golden/G2StateMachineConfiguration"), "G2StateMachineConfiguration" + ), + new TestScenario( + "Extended Analysis Sample", + root.resolve("state_machines/extended_analysis_sample"), + Path.of("src/test/resources/golden/ExtendedStateMachineConfig"), + "ExtendedStateMachineConfig" + ), + new TestScenario( + "Extended Analysis Sample (PROD)", + root.resolve("state_machines/extended_analysis_sample"), + Path.of("src/test/resources/golden/ExtendedStateMachineConfig_PROD"), + "ExtendedStateMachineConfig", + List.of("prod") + ), + new TestScenario( + "Inheritance Sample", + root.resolve("state_machines/inheritance_sample"), + Path.of("src/test/resources/golden/InheritanceStateMachineConfig"), + "InheritanceStateMachineConfig" + ), + new TestScenario( + "Enterprise Order System", + root.resolve("state_machines/enterprise_order_system"), + Path.of("src/test/resources/golden/EnterpriseStateMachineConfig"), + "EnterpriseStateMachineConfig" ) ); } @@ -108,7 +132,12 @@ public class RegressionTest { @ParameterizedTest(name = "{0}") @MethodSource("provideTestScenarios") void testOutputMatchesGolden(TestScenario scenario, @TempDir Path tempDir) throws Exception { - exportService.runExporter(scenario.inputPath(), tempDir, null, true); + System.out.println("Running test for " + scenario.name()); + if (exportService == null) System.out.println("exportService is NULL"); + if (scenario.inputPath() == null) System.out.println("inputPath is NULL"); + if (tempDir == null) System.out.println("tempDir is NULL"); + + exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles()); // Find the generated directory (it might be named with FQN) List generatedDirs; @@ -121,28 +150,33 @@ public class RegressionTest { .filter(d -> d.getFileName().toString().endsWith(targetBaseName)) .findFirst() .orElseThrow(() -> new IllegalArgumentException("No output directory found for " + targetBaseName + " in " + generatedDirs)); +String actualBaseName = actualDir.getFileName().toString(); - String actualBaseName = actualDir.getFileName().toString(); +List generatedFiles; +try (var stream = Files.list(actualDir)) { + generatedFiles = stream.filter(Files::isRegularFile).toList(); +} - for (StateMachineExporter exporter : exporters) { - String fileName = actualBaseName + exporter.getFileExtension(); - String goldenFileName = targetBaseName + exporter.getFileExtension(); +for (Path actualFile : generatedFiles) { + String fileName = actualFile.getFileName().toString(); + // Map actual filename to golden filename (handle FQN difference) + String goldenFileName = fileName.replace(actualBaseName, targetBaseName); + Path goldenFile = scenario.goldenPath().resolve(goldenFileName); - Path actualFile = actualDir.resolve(fileName); - Path goldenFile = scenario.goldenPath().resolve(goldenFileName); + if (System.getProperty("updateGolden", "false").equals("true")) { + Files.createDirectories(scenario.goldenPath()); + Files.copy(actualFile, goldenFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + System.out.println("Updated golden file: " + goldenFile); + } else { + assertThat(goldenFile) + .as("Golden file missing for %s", fileName) + .exists(); - if (Boolean.getBoolean("updateGolden")) { - Files.createDirectories(goldenFile.getParent()); - Files.copy(actualFile, goldenFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - } else { - assertThat(actualFile) - .as("File %s should exist in output", fileName) - .exists(); - - assertThat(actualFile) - .as("Content mismatch for %s", fileName) - .content().isEqualTo(Files.readString(goldenFile)); - } - } + assertThat(actualFile) + .as("Content mismatch for %s", fileName) + .content().isEqualTo(Files.readString(goldenFile)); } } +} +} + diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/AnalysisFoundationTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/AnalysisFoundationTest.java new file mode 100644 index 0000000..750d7ca --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/AnalysisFoundationTest.java @@ -0,0 +1,83 @@ +package click.kamil.springstatemachineexporter.analysis; + +import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver; +import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +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; + +public class AnalysisFoundationTest { + + @Test + void testPropertyResolver(@TempDir Path tempDir) throws IOException { + Files.writeString(tempDir.resolve("application.properties"), "app.event=SUBMIT\napp.path=/api/v1"); + + PropertyResolver resolver = new PropertyResolver(); + Map props = resolver.resolveProperties(tempDir); + + assertThat(props).containsEntry("app.event", "SUBMIT"); + assertThat(props).containsEntry("app.path", "/api/v1"); + } + + @Test + void testConstantResolverManualFallback(@TempDir Path tempDir) throws IOException { + // Create a class with a constant + Path constDir = tempDir.resolve("com/example"); + Files.createDirectories(constDir); + Files.writeString(constDir.resolve("Events.java"), + "package com.example;\n" + + "public class Events {\n" + + " public static final String SUBMIT = \"SUBMIT_EVENT\";\n" + + "}"); + + // Create a class that uses the constant + Files.writeString(constDir.resolve("Usage.java"), + "package com.example;\n" + + "public class Usage {\n" + + " String e = Events.SUBMIT;\n" + + "}"); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + TypeDeclaration usageTd = context.getTypeDeclaration("com.example.Usage"); + FieldDeclaration field = usageTd.getFields()[0]; + VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0); + Expression initializer = fragment.getInitializer(); + + ConstantResolver resolver = new ConstantResolver(); + String value = resolver.resolve(initializer, context); + + assertThat(value).isEqualTo("SUBMIT_EVENT"); + } + + @Test + void testPropertyResolverWithProfiles(@TempDir Path tempDir) throws IOException { + Files.writeString(tempDir.resolve("application.properties"), "app.event=BASE"); + Files.writeString(tempDir.resolve("application-prod.properties"), "app.event=PROD"); + + PropertyResolver resolver = new PropertyResolver(); + + // 1. Base only + Map baseProps = resolver.resolveProperties(tempDir); + assertThat(baseProps).containsEntry("app.event", "BASE"); + + // 2. With prod profile + resolver.setActiveProfiles(List.of("prod")); + Map prodProps = resolver.resolveProperties(tempDir); + assertThat(prodProps).containsEntry("app.event", "PROD"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/exporter/PlantUmlCustomStyleTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/exporter/PlantUmlCustomStyleTest.java deleted file mode 100644 index f4d0422..0000000 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/exporter/PlantUmlCustomStyleTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package click.kamil.springstatemachineexporter.exporter; - -import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; -import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata; -import click.kamil.springstatemachineexporter.model.State; -import click.kamil.springstatemachineexporter.model.Transition; -import click.kamil.springstatemachineexporter.model.TransitionType; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -class PlantUmlCustomStyleTest { - - @Test - void shouldApplyCustomStyleFromMetadata() { - PlantUml exporter = new PlantUml(); - Transition transition = new Transition(); - transition.setType(TransitionType.EXTERNAL); - transition.setSourceStates(List.of(State.of("S1"))); - transition.setTargetStates(List.of(State.of("S2"))); - transition.setEvent("E1"); - - AnalysisResult result = AnalysisResult.builder() - .name("TestSM") - .transitions(List.of(transition)) - .startStates(Set.of("S1")) - .endStates(Set.of("S2")) - .metadata(CodebaseMetadata.builder() - .properties(Map.of("plantuml.style", "skinparam stateBackgroundColor Red")) - .build()) - .build(); - - String puml = exporter.export(result, ExportOptions.builder().build()); - - assertThat(puml) - .contains("skinparam stateBackgroundColor Red") - .contains("@enduml"); - - // Ensure it's inserted before @enduml - int styleIndex = puml.indexOf("skinparam stateBackgroundColor Red"); - int endumlIndex = puml.indexOf("@enduml"); - assertThat(styleIndex).isLessThan(endumlIndex); - } -} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/service/JsonImportServiceTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/service/JsonImportServiceTest.java index ae903e9..53d09b3 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/service/JsonImportServiceTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/service/JsonImportServiceTest.java @@ -1,5 +1,6 @@ package click.kamil.springstatemachineexporter.service; +import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; import click.kamil.springstatemachineexporter.model.StateMachineModel; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -39,4 +40,33 @@ class JsonImportServiceTest { assertThat(model.getStartStates()).containsExactly("S1"); assertThat(model.getEndStates()).containsExactly("S2"); } + + @Test + void shouldImportRichAnalysisResultFromJson(@TempDir Path tempDir) throws IOException { + Path jsonFile = tempDir.resolve("rich.json"); + String json = """ + { + "name" : "RichSM", + "transitions" : [], + "startStates" : [], + "endStates" : [], + "metadata" : { + "entryPoints" : [ { + "type" : "REST", + "name" : "POST /rich", + "className" : "RichController", + "methodName" : "submit" + } ] + } + } + """; + Files.writeString(jsonFile, json); + + JsonImportService importService = new JsonImportService(); + AnalysisResult result = importService.importAnalysisResult(jsonFile); + + assertThat(result.getName()).isEqualTo("RichSM"); + assertThat(result.getMetadata().getEntryPoints()).hasSize(1); + assertThat(result.getMetadata().getEntryPoints().get(0).getName()).isEqualTo("POST /rich"); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/utils/TestScenario.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/utils/TestScenario.java index 4970f46..dcc65a3 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/utils/TestScenario.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/utils/TestScenario.java @@ -1,10 +1,17 @@ package click.kamil.springstatemachineexporter.utils; import java.nio.file.Path; +import java.util.Collections; +import java.util.List; public record TestScenario( String name, Path inputPath, Path goldenPath, - String baseName -) {} + String baseName, + List activeProfiles +) { + public TestScenario(String name, Path inputPath, Path goldenPath, String baseName) { + this(name, inputPath, goldenPath, baseName, Collections.emptyList()); + } +} diff --git a/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.json index e963af9..d510c35 100644 --- a/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.json @@ -1,5 +1,15 @@ { + "metadata" : { + "triggers" : [ ], + "entryPoints" : [ ], + "callChains" : [ ], + "properties" : { + "spring.application.name" : "statemachinedemo" + } + }, "name" : "click.kamil.examples.statemachine.complex.ComplexStateMachineConfig", + "renderChoicesAsDiamonds" : true, + "startStates" : [ "States.STATE1" ], "transitions" : [ { "type" : "EXTERNAL", "sourceStates" : [ { @@ -572,7 +582,5 @@ "actions" : [ ], "order" : 1 } ], - "startStates" : [ "States.STATE1" ], - "endStates" : [ ], - "renderChoicesAsDiamonds" : true + "endStates" : [ ] } diff --git a/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.png b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.png index 1283c7f..fe07a47 100644 Binary files a/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.png and b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.png differ diff --git a/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.puml b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.puml index 84428d8..1537e6c 100644 --- a/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.puml +++ b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.puml @@ -1,4 +1,28 @@ @startuml +!pragma layout smetana +hide empty description +hide stereotype +state STATE1 +state STATE2 +state STATE3 +state STATE4 +state STATE5 +state STATE6 +state STATE7 +state STATE8 +state STATE9 +state STATE10 +state STATE11 +state STATE12 +state STATE13 +state STATE14 +state STATE15 +state STATE16 +state STATE17 +state STATE18 +state STATE19 +state STATE20 + + +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype + +[*] --> NEW + +state CHECK_AVAILABILITY <> + +NEW -[#1E90FF,bold]-> CHECK_AVAILABILITY <> <> : PLACE_ORDER +CHECK_AVAILABILITY -[#FF6347,bold]-> PENDING_PAYMENT <> : [λ] (order=0) +CHECK_AVAILABILITY -[#FF6347,bold]-> CANCELLED <> : (order=1) +PENDING_PAYMENT -[#1E90FF,bold]-> PAID <> <> : PAY_ORDER +PAID -[#1E90FF,bold]-> SHIPPED <> <> : SHIP_ORDER +SHIPPED -[#1E90FF,bold]-> DELIVERED <> <> : FINALIZE +PAID -[#1E90FF,bold]-> CANCELLED <> <> : CANCEL_ORDER +DELIVERED -[#1E90FF,bold]-> RETURNED <> <> : RETURN_ORDER +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype + +CANCELLED --> [*] +DELIVERED --> [*] +RETURNED --> [*] +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.scxml.xml b/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.scxml.xml new file mode 100644 index 0000000..367c9bf --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.scxml.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.dot b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.dot new file mode 100644 index 0000000..a13f5ef --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.dot @@ -0,0 +1,17 @@ +digraph statemachine { + rankdir=LR; + node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"]; + edge [fontname="Arial", fontsize=10]; + + _start [shape=circle, label="", fillcolor=black, width=0.1]; + _start -> INIT_STATE; + CANCELLED [fillcolor=lightgray]; + COMPLETED [fillcolor=lightgray]; + START -> PROCESSING [label="SUBMIT_EVENT", style="solid", color="black"]; + PROCESSING -> COMPLETED [label="FINISH", style="solid", color="black"]; + PROCESSING -> CANCELLED [label="CANCEL_EVENT", style="solid", color="black"]; + START -> PROCESSING [label="REACTIVE_EVENT", style="solid", color="black"]; + START -> START [label="AUDIT_EVENT", style="solid", color="black"]; + START -> PROCESSING [label="EXTERNAL_TRIGGER", style="solid", color="black"]; +} + diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json new file mode 100644 index 0000000..0c6b2c3 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json @@ -0,0 +1,340 @@ +{ + "metadata" : { + "triggers" : [ { + "event" : "FINISH", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "finishOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 34 + }, { + "event" : "AUDIT_EVENT", + "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor", + "methodName" : "preHandle", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 16 + }, { + "event" : "EXTERNAL_TRIGGER", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "processSubmit", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 19 + }, { + "event" : "SUBMIT_EVENT", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "processSubmit", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 20 + }, { + "event" : "CANCEL_EVENT", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "processCancel", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 25 + }, { + "event" : "[LIFECYCLE:RESTORE]", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "resumeOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 29 + }, { + "event" : "REACTIVE_EVENT", + "className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService", + "methodName" : "processReactive", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 18 + } ], + "entryPoints" : [ { + "type" : "REST", + "name" : "POST /api/orders/submit", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/api/orders/submit", + "verb" : "POST" + } + }, { + "type" : "REST", + "name" : "POST /api/orders/cancel", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "cancelOrder", + "metadata" : { + "path" : "/api/orders/cancel", + "verb" : "POST" + } + }, { + "type" : "REST", + "name" : "POST /api/orders/finish", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "finishOrder", + "metadata" : { + "path" : "/api/orders/finish", + "verb" : "POST" + } + }, { + "type" : "REST", + "name" : "POST /api/orders/resume", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "resumeOrder", + "metadata" : { + "path" : "/api/orders/resume", + "verb" : "POST" + } + }, { + "type" : "REST", + "name" : "POST /api/orders/reactive", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "reactiveOrder", + "metadata" : { + "path" : "/api/orders/reactive", + "verb" : "POST" + } + }, { + "type" : "CUSTOM", + "name" : "INTERCEPTOR: AuditInterceptor.preHandle", + "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor", + "methodName" : "preHandle", + "metadata" : { + "interceptorType" : "Spring MVC Interceptor" + } + } ], + "callChains" : [ { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/submit", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/api/orders/submit", + "verb" : "POST" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ], + "triggerPoint" : { + "event" : "EXTERNAL_TRIGGER", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "processSubmit", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 19 + } + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/submit", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/api/orders/submit", + "verb" : "POST" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ], + "triggerPoint" : { + "event" : "SUBMIT_EVENT", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "processSubmit", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 20 + } + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/cancel", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "cancelOrder", + "metadata" : { + "path" : "/api/orders/cancel", + "verb" : "POST" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.cancelOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processCancel" ], + "triggerPoint" : { + "event" : "CANCEL_EVENT", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "processCancel", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 25 + } + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/finish", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "finishOrder", + "metadata" : { + "path" : "/api/orders/finish", + "verb" : "POST" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.finishOrder" ], + "triggerPoint" : { + "event" : "FINISH", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "finishOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 34 + } + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/resume", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "resumeOrder", + "metadata" : { + "path" : "/api/orders/resume", + "verb" : "POST" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.resumeOrder", "click.kamil.examples.statemachine.extended.service.OrderService.resumeOrder" ], + "triggerPoint" : { + "event" : "[LIFECYCLE:RESTORE]", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "resumeOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 29 + } + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/reactive", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "reactiveOrder", + "metadata" : { + "path" : "/api/orders/reactive", + "verb" : "POST" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.reactiveOrder", "click.kamil.examples.statemachine.extended.service.ReactiveOrderService.processReactive" ], + "triggerPoint" : { + "event" : "REACTIVE_EVENT", + "className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService", + "methodName" : "processReactive", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 18 + } + }, { + "entryPoint" : { + "type" : "CUSTOM", + "name" : "INTERCEPTOR: AuditInterceptor.preHandle", + "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor", + "methodName" : "preHandle", + "metadata" : { + "interceptorType" : "Spring MVC Interceptor" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.AuditInterceptor.preHandle" ], + "triggerPoint" : { + "event" : "AUDIT_EVENT", + "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor", + "methodName" : "preHandle", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 16 + } + } ], + "properties" : { + "app.states.initial" : "INIT_STATE" + } + }, + "name" : "click.kamil.examples.statemachine.extended.config.ExtendedStateMachineConfig", + "renderChoicesAsDiamonds" : true, + "startStates" : [ "INIT_STATE" ], + "transitions" : [ { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"START\"", + "fullIdentifier" : "START" + } ], + "targetStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "event" : "SUBMIT_EVENT", + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "targetStates" : [ { + "rawName" : "\"COMPLETED\"", + "fullIdentifier" : "COMPLETED" + } ], + "event" : "FINISH", + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "targetStates" : [ { + "rawName" : "\"CANCELLED\"", + "fullIdentifier" : "CANCELLED" + } ], + "event" : "CANCEL_EVENT", + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"START\"", + "fullIdentifier" : "START" + } ], + "targetStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "event" : "REACTIVE_EVENT", + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"START\"", + "fullIdentifier" : "START" + } ], + "targetStates" : [ { + "rawName" : "\"START\"", + "fullIdentifier" : "START" + } ], + "event" : "AUDIT_EVENT", + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"START\"", + "fullIdentifier" : "START" + } ], + "targetStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "event" : "EXTERNAL_TRIGGER", + "guard" : null, + "actions" : [ ], + "order" : null + } ], + "endStates" : [ "CANCELLED", "COMPLETED" ] +} diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.png b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.png new file mode 100644 index 0000000..0c6feee Binary files /dev/null and b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.png differ diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.puml b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.puml new file mode 100644 index 0000000..ee671a1 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.puml @@ -0,0 +1,70 @@ +@startuml +!pragma layout smetana +hide empty description +hide stereotype +state START +state PROCESSING +state COMPLETED +state CANCELLED + + + +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype + +[*] --> INIT_STATE + + +START -[#1E90FF,bold]-> PROCESSING <> <> : SUBMIT_EVENT +PROCESSING -[#1E90FF,bold]-> COMPLETED <> <> : FINISH +PROCESSING -[#1E90FF,bold]-> CANCELLED <> <> : CANCEL_EVENT +START -[#1E90FF,bold]-> PROCESSING <> <> : REACTIVE_EVENT +START -[#1E90FF,bold]-> START <> <> : AUDIT_EVENT +START -[#1E90FF,bold]-> PROCESSING <> <> : EXTERNAL_TRIGGER +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype + +CANCELLED --> [*] +COMPLETED --> [*] +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.scxml.xml b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.scxml.xml new file mode 100644 index 0000000..9094d51 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.scxml.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.dot b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.dot new file mode 100644 index 0000000..135e343 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.dot @@ -0,0 +1,17 @@ +digraph statemachine { + rankdir=LR; + node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"]; + edge [fontname="Arial", fontsize=10]; + + _start [shape=circle, label="", fillcolor=black, width=0.1]; + _start -> PROD_INITIAL; + CANCELLED [fillcolor=lightgray]; + COMPLETED [fillcolor=lightgray]; + START -> PROCESSING [label="SUBMIT_EVENT", style="solid", color="black"]; + PROCESSING -> COMPLETED [label="FINISH", style="solid", color="black"]; + PROCESSING -> CANCELLED [label="CANCEL_EVENT", style="solid", color="black"]; + START -> PROCESSING [label="REACTIVE_EVENT", style="solid", color="black"]; + START -> START [label="AUDIT_EVENT", style="solid", color="black"]; + START -> PROCESSING [label="EXTERNAL_TRIGGER", style="solid", color="black"]; +} + diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json new file mode 100644 index 0000000..35c6582 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json @@ -0,0 +1,340 @@ +{ + "metadata" : { + "triggers" : [ { + "event" : "FINISH", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "finishOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 34 + }, { + "event" : "AUDIT_EVENT", + "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor", + "methodName" : "preHandle", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 16 + }, { + "event" : "EXTERNAL_TRIGGER", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "processSubmit", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 19 + }, { + "event" : "SUBMIT_EVENT", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "processSubmit", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 20 + }, { + "event" : "CANCEL_EVENT", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "processCancel", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 25 + }, { + "event" : "[LIFECYCLE:RESTORE]", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "resumeOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 29 + }, { + "event" : "REACTIVE_EVENT", + "className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService", + "methodName" : "processReactive", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 18 + } ], + "entryPoints" : [ { + "type" : "REST", + "name" : "POST /api/orders/submit", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/api/orders/submit", + "verb" : "POST" + } + }, { + "type" : "REST", + "name" : "POST /api/orders/cancel", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "cancelOrder", + "metadata" : { + "path" : "/api/orders/cancel", + "verb" : "POST" + } + }, { + "type" : "REST", + "name" : "POST /api/orders/finish", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "finishOrder", + "metadata" : { + "path" : "/api/orders/finish", + "verb" : "POST" + } + }, { + "type" : "REST", + "name" : "POST /api/orders/resume", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "resumeOrder", + "metadata" : { + "path" : "/api/orders/resume", + "verb" : "POST" + } + }, { + "type" : "REST", + "name" : "POST /api/orders/reactive", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "reactiveOrder", + "metadata" : { + "path" : "/api/orders/reactive", + "verb" : "POST" + } + }, { + "type" : "CUSTOM", + "name" : "INTERCEPTOR: AuditInterceptor.preHandle", + "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor", + "methodName" : "preHandle", + "metadata" : { + "interceptorType" : "Spring MVC Interceptor" + } + } ], + "callChains" : [ { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/submit", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/api/orders/submit", + "verb" : "POST" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ], + "triggerPoint" : { + "event" : "EXTERNAL_TRIGGER", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "processSubmit", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 19 + } + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/submit", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/api/orders/submit", + "verb" : "POST" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ], + "triggerPoint" : { + "event" : "SUBMIT_EVENT", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "processSubmit", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 20 + } + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/cancel", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "cancelOrder", + "metadata" : { + "path" : "/api/orders/cancel", + "verb" : "POST" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.cancelOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processCancel" ], + "triggerPoint" : { + "event" : "CANCEL_EVENT", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "processCancel", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 25 + } + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/finish", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "finishOrder", + "metadata" : { + "path" : "/api/orders/finish", + "verb" : "POST" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.finishOrder" ], + "triggerPoint" : { + "event" : "FINISH", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "finishOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 34 + } + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/resume", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "resumeOrder", + "metadata" : { + "path" : "/api/orders/resume", + "verb" : "POST" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.resumeOrder", "click.kamil.examples.statemachine.extended.service.OrderService.resumeOrder" ], + "triggerPoint" : { + "event" : "[LIFECYCLE:RESTORE]", + "className" : "click.kamil.examples.statemachine.extended.service.OrderService", + "methodName" : "resumeOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 29 + } + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/reactive", + "className" : "click.kamil.examples.statemachine.extended.web.OrderController", + "methodName" : "reactiveOrder", + "metadata" : { + "path" : "/api/orders/reactive", + "verb" : "POST" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.reactiveOrder", "click.kamil.examples.statemachine.extended.service.ReactiveOrderService.processReactive" ], + "triggerPoint" : { + "event" : "REACTIVE_EVENT", + "className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService", + "methodName" : "processReactive", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 18 + } + }, { + "entryPoint" : { + "type" : "CUSTOM", + "name" : "INTERCEPTOR: AuditInterceptor.preHandle", + "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor", + "methodName" : "preHandle", + "metadata" : { + "interceptorType" : "Spring MVC Interceptor" + } + }, + "methodChain" : [ "click.kamil.examples.statemachine.extended.web.AuditInterceptor.preHandle" ], + "triggerPoint" : { + "event" : "AUDIT_EVENT", + "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor", + "methodName" : "preHandle", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 16 + } + } ], + "properties" : { + "app.states.initial" : "PROD_INITIAL" + } + }, + "name" : "click.kamil.examples.statemachine.extended.config.ExtendedStateMachineConfig", + "renderChoicesAsDiamonds" : true, + "startStates" : [ "PROD_INITIAL" ], + "transitions" : [ { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"START\"", + "fullIdentifier" : "START" + } ], + "targetStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "event" : "SUBMIT_EVENT", + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "targetStates" : [ { + "rawName" : "\"COMPLETED\"", + "fullIdentifier" : "COMPLETED" + } ], + "event" : "FINISH", + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "targetStates" : [ { + "rawName" : "\"CANCELLED\"", + "fullIdentifier" : "CANCELLED" + } ], + "event" : "CANCEL_EVENT", + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"START\"", + "fullIdentifier" : "START" + } ], + "targetStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "event" : "REACTIVE_EVENT", + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"START\"", + "fullIdentifier" : "START" + } ], + "targetStates" : [ { + "rawName" : "\"START\"", + "fullIdentifier" : "START" + } ], + "event" : "AUDIT_EVENT", + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"START\"", + "fullIdentifier" : "START" + } ], + "targetStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "event" : "EXTERNAL_TRIGGER", + "guard" : null, + "actions" : [ ], + "order" : null + } ], + "endStates" : [ "CANCELLED", "COMPLETED" ] +} diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.png b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.png new file mode 100644 index 0000000..8727b0c Binary files /dev/null and b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.png differ diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.puml b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.puml new file mode 100644 index 0000000..79e2ae3 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.puml @@ -0,0 +1,70 @@ +@startuml +!pragma layout smetana +hide empty description +hide stereotype +state START +state PROCESSING +state COMPLETED +state CANCELLED + + + +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype + +[*] --> PROD_INITIAL + + +START -[#1E90FF,bold]-> PROCESSING <> <> : SUBMIT_EVENT +PROCESSING -[#1E90FF,bold]-> COMPLETED <> <> : FINISH +PROCESSING -[#1E90FF,bold]-> CANCELLED <> <> : CANCEL_EVENT +START -[#1E90FF,bold]-> PROCESSING <> <> : REACTIVE_EVENT +START -[#1E90FF,bold]-> START <> <> : AUDIT_EVENT +START -[#1E90FF,bold]-> PROCESSING <> <> : EXTERNAL_TRIGGER +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype + +CANCELLED --> [*] +COMPLETED --> [*] +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.scxml.xml b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.scxml.xml new file mode 100644 index 0000000..a99e0dd --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.scxml.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.json b/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.json index 55373b1..047c356 100644 --- a/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.json +++ b/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.json @@ -1,5 +1,15 @@ { + "metadata" : { + "triggers" : [ ], + "entryPoints" : [ ], + "callChains" : [ ], + "properties" : { + "spring.application.name" : "statemachinedemo" + } + }, "name" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.F1StateMachineConfiguration", + "renderChoicesAsDiamonds" : true, + "startStates" : [ "States.STATE1" ], "transitions" : [ { "type" : "EXTERNAL", "sourceStates" : [ { @@ -774,7 +784,5 @@ "actions" : [ ], "order" : null } ], - "startStates" : [ "States.STATE1" ], - "endStates" : [ "States.STATEZ" ], - "renderChoicesAsDiamonds" : true + "endStates" : [ "States.STATEZ" ] } diff --git a/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.png b/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.png index 05ce3f4..ab9e524 100644 Binary files a/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.png and b/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.png differ diff --git a/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.puml b/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.puml index 81e0d2d..48c76dd 100644 --- a/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.puml +++ b/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.puml @@ -1,4 +1,35 @@ @startuml +!pragma layout smetana +hide empty description +hide stereotype +state STATE1 +state STATE2 +state STATE3 +state STATE4 +state STATE5 +state STATE6 +state STATE7 +state STATE8 +state STATE9 +state STATE10 +state STATE11 +state STATE12 +state STATE13 +state STATE14 +state STATE15 +state STATE16 +state CANCEL +state STATEY +state STATEX +state STATEZ +state STATE17 +state STATE18 +state STATE19 +state STATE20 +state STATE_EXTRA_1 +state STATE_EXTRA_3 +state STATE_EXTRA_2 + + +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype +hide <> stereotype + +[*] --> START + + +START -[#1E90FF,bold]-> WORKING <> <> : INHERITED_SUBMIT +hide <> stereotype + +WORKING --> [*] +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.scxml.xml b/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.scxml.xml new file mode 100644 index 0000000..3f7965e --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.scxml.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.json b/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.json index 45520e0..2d25622 100644 --- a/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.json +++ b/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.json @@ -1,5 +1,15 @@ { + "metadata" : { + "triggers" : [ ], + "entryPoints" : [ ], + "callChains" : [ ], + "properties" : { + "spring.application.name" : "statemachinedemo" + } + }, "name" : "click.kamil.examples.statemachine.enumstate.KamilEnumStateMachineConfiguration", + "renderChoicesAsDiamonds" : true, + "startStates" : [ "OrderStates.SUBMITTED" ], "transitions" : [ { "type" : "EXTERNAL", "sourceStates" : [ { @@ -195,7 +205,5 @@ } ], "order" : null } ], - "startStates" : [ "OrderStates.SUBMITTED" ], - "endStates" : [ "OrderStates.CANCELED", "OrderStates.FULFILLED" ], - "renderChoicesAsDiamonds" : true + "endStates" : [ "OrderStates.CANCELED", "OrderStates.FULFILLED" ] } diff --git a/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.png b/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.png index 9aba9b7..6f7b989 100644 Binary files a/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.png and b/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.png differ diff --git a/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.puml b/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.puml index c73e397..1996707 100644 --- a/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.puml +++ b/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.puml @@ -1,4 +1,16 @@ @startuml +!pragma layout smetana +hide empty description +hide stereotype +state SUBMITTED +state PAID +state FULFILLED +state CANCELED +state PAID1 +state PAID2 +state PAID3 +state HAPPEN +