v2.4 extended analysis render update

This commit is contained in:
2026-06-16 18:32:11 +02:00
parent ac05513b6c
commit a9d40aaf8c
15 changed files with 1661 additions and 39 deletions

View File

@@ -15,7 +15,7 @@ public class PropertyEnricher implements AnalysisEnricher {
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) { public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
log.info("Enriching {} with properties", result.getName()); log.info("Enriching {} with properties", result.getName());
Map<String, String> properties = intelligence.resolveProperties(); Map<String, Map<String, String>> properties = intelligence.resolveProperties();
result.addMetadata(CodebaseMetadata.builder() result.addMetadata(CodebaseMetadata.builder()
.properties(properties) .properties(properties)

View File

@@ -6,31 +6,96 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import click.kamil.springstatemachineexporter.model.Transition; import click.kamil.springstatemachineexporter.model.Transition;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import lombok.extern.jackson.Jacksonized; import lombok.extern.jackson.Jacksonized;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
@Getter @Getter
@Builder @Setter
@Builder(toBuilder = true)
@Jacksonized @Jacksonized
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public class AnalysisResult { public class AnalysisResult {
private final String name; private String name;
@Builder.Default @Builder.Default
private final Set<click.kamil.springstatemachineexporter.model.State> states = java.util.Collections.emptySet(); private Set<click.kamil.springstatemachineexporter.model.State> states = java.util.Collections.emptySet();
private final List<Transition> transitions; private List<Transition> transitions;
private final Set<String> startStates; private Set<String> startStates;
private final Set<String> endStates; private Set<String> endStates;
private final boolean renderChoicesAsDiamonds; private boolean renderChoicesAsDiamonds;
@Builder.Default @Builder.Default
private final List<BusinessFlow> flows = java.util.Collections.emptyList(); private List<BusinessFlow> flows = java.util.Collections.emptyList();
@Builder.Default @Builder.Default
private CodebaseMetadata metadata = CodebaseMetadata.empty(); private CodebaseMetadata metadata = CodebaseMetadata.empty();
public void applyResolution(Map<String, String> properties) {
var resolver = new click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver();
// 1. Resolve start/end states strings
if (startStates != null) {
this.startStates = startStates.stream()
.map(s -> resolver.resolveValue(s, properties))
.collect(java.util.stream.Collectors.toSet());
}
if (endStates != null) {
this.endStates = endStates.stream()
.map(s -> resolver.resolveValue(s, properties))
.collect(java.util.stream.Collectors.toSet());
}
// 2. Resolve states (State record is immutable, so recreate)
if (states != null) {
this.states = states.stream()
.map(s -> click.kamil.springstatemachineexporter.model.State.of(s.rawName(), resolver.resolveValue(s.fullIdentifier(), properties)))
.collect(java.util.stream.Collectors.toSet());
}
// 3. Resolve transitions
if (transitions != null) {
for (var t : transitions) {
if (t.getEvent() != null) {
t.setEvent(resolver.resolveValue(t.getEvent(), properties));
}
// Resolve states within transitions
t.setSourceStates(t.getSourceStates().stream()
.map(s -> click.kamil.springstatemachineexporter.model.State.of(s.rawName(), resolver.resolveValue(s.fullIdentifier(), properties)))
.collect(java.util.stream.Collectors.toList()));
t.setTargetStates(t.getTargetStates().stream()
.map(s -> click.kamil.springstatemachineexporter.model.State.of(s.rawName(), resolver.resolveValue(s.fullIdentifier(), properties)))
.collect(java.util.stream.Collectors.toList()));
}
}
// 4. Resolve metadata (triggers, entry points)
if (metadata != null) {
if (metadata.getTriggers() != null) {
for (var trigger : metadata.getTriggers()) {
if (trigger.getEvent() != null) {
trigger.setEvent(resolver.resolveValue(trigger.getEvent(), properties));
}
}
}
if (metadata.getEntryPoints() != null) {
for (var ep : metadata.getEntryPoints()) {
if (ep.getName() != null) {
ep.setName(resolver.resolveValue(ep.getName(), properties));
}
if (ep.getMetadata() != null) {
for (var entry : ep.getMetadata().entrySet()) {
entry.setValue(resolver.resolveValue(entry.getValue(), properties));
}
}
}
}
}
}
public void addMetadata(CodebaseMetadata newMetadata) { public void addMetadata(CodebaseMetadata newMetadata) {
if (newMetadata == null) return; if (newMetadata == null) return;
@@ -43,7 +108,7 @@ public class AnalysisResult {
List<CallChain> combinedCallChains = new java.util.ArrayList<>(this.metadata.getCallChains()); List<CallChain> combinedCallChains = new java.util.ArrayList<>(this.metadata.getCallChains());
if (newMetadata.getCallChains() != null) combinedCallChains.addAll(newMetadata.getCallChains()); if (newMetadata.getCallChains() != null) combinedCallChains.addAll(newMetadata.getCallChains());
java.util.Map<String, String> combinedProperties = new java.util.HashMap<>(this.metadata.getProperties()); java.util.Map<String, java.util.Map<String, String>> combinedProperties = new java.util.HashMap<>(this.metadata.getProperties());
if (newMetadata.getProperties() != null) combinedProperties.putAll(newMetadata.getProperties()); if (newMetadata.getProperties() != null) combinedProperties.putAll(newMetadata.getProperties());
this.metadata = CodebaseMetadata.builder() this.metadata = CodebaseMetadata.builder()

View File

@@ -22,7 +22,7 @@ public class CodebaseMetadata {
@Builder.Default @Builder.Default
private final List<CallChain> callChains = Collections.emptyList(); private final List<CallChain> callChains = Collections.emptyList();
@Builder.Default @Builder.Default
private final Map<String, String> properties = Collections.emptyMap(); private final Map<String, Map<String, String>> properties = Collections.emptyMap();
public static CodebaseMetadata empty() { public static CodebaseMetadata empty() {
return CodebaseMetadata.builder().build(); return CodebaseMetadata.builder().build();

View File

@@ -26,10 +26,10 @@ public class EntryPoint {
} }
private final Type type; private final Type type;
private final String name; // e.g., "POST /orders" or "Kafka Topic: orders" private String name; // e.g., "POST /orders" or "Kafka Topic: orders"
private final String className; private final String className;
private final String methodName; private final String methodName;
private final Map<String, String> metadata; // e.g., path, topic, exchange private Map<String, String> metadata; // e.g., path, topic, exchange
@Builder.Default @Builder.Default
private final List<Parameter> parameters = java.util.Collections.emptyList(); private final List<Parameter> parameters = java.util.Collections.emptyList();
} }

View File

@@ -12,7 +12,7 @@ import java.util.Map;
@Jacksonized @Jacksonized
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public class TriggerPoint { public class TriggerPoint {
private final String event; private String event;
private final String className; private final String className;
private final String methodName; private final String methodName;
private final String sourceModule; private final String sourceModule;

View File

@@ -42,6 +42,37 @@ public class PropertyResolver {
return allProperties; return allProperties;
} }
public Map<String, Map<String, String>> resolveAllProperties(Path rootDir) {
Map<String, Map<String, String>> profiles = new HashMap<>();
// 1. Load default profile
Map<String, String> defaultProps = new HashMap<>();
loadMatching(rootDir, "application.properties", defaultProps);
loadMatching(rootDir, "application.yml", defaultProps);
loadMatching(rootDir, "application.yaml", defaultProps);
profiles.put("default", defaultProps);
// 2. Discover and load all other profiles
try (Stream<Path> paths = Files.walk(rootDir)) {
paths.filter(Files::isRegularFile)
.filter(p -> {
String name = p.getFileName().toString();
return name.startsWith("application-") &&
(name.endsWith(".properties") || name.endsWith(".yml") || name.endsWith(".yaml"));
})
.forEach(p -> {
String name = p.getFileName().toString();
String profile = name.substring("application-".length(), name.lastIndexOf('.'));
Map<String, String> loaded = p.toString().endsWith(".properties") ? loadProperties(p) : loadYaml(p);
profiles.computeIfAbsent(profile, k -> new HashMap<>()).putAll(loaded);
});
} catch (IOException e) {
log.error("Failed to scan for profiles in {}", rootDir, e);
}
return profiles;
}
private void loadMatching(Path rootDir, String fileName, Map<String, String> target) { private void loadMatching(Path rootDir, String fileName, Map<String, String> target) {
try (Stream<Path> paths = Files.walk(rootDir)) { try (Stream<Path> paths = Files.walk(rootDir)) {
paths.filter(p -> Files.isRegularFile(p)) paths.filter(p -> Files.isRegularFile(p))

View File

@@ -24,7 +24,7 @@ public interface CodebaseIntelligenceProvider {
List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers); List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers);
/** /**
* Resolves configuration properties. * Resolves configuration properties per profile.
*/ */
Map<String, String> resolveProperties(); Map<String, Map<String, String>> resolveProperties();
} }

View File

@@ -70,7 +70,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
} }
@Override @Override
public Map<String, String> resolveProperties() { public Map<String, Map<String, String>> resolveProperties() {
return context.getProperties(); return context.getProperties();
} }
} }

View File

@@ -27,7 +27,7 @@ public class CodebaseContext {
private final Set<String> ambiguousSimpleNames = new HashSet<>(); private final Set<String> ambiguousSimpleNames = new HashSet<>();
private final ConstantResolver constantResolver = new ConstantResolver(); private final ConstantResolver constantResolver = new ConstantResolver();
private final PropertyResolver propertyResolver = new PropertyResolver(); private final PropertyResolver propertyResolver = new PropertyResolver();
private Map<String, String> properties = new HashMap<>(); private Map<String, Map<String, String>> allProperties = new HashMap<>();
private List<LibraryHint> libraryHints = new ArrayList<>(); private List<LibraryHint> libraryHints = new ArrayList<>();
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
@@ -48,8 +48,8 @@ public class CodebaseContext {
private String[] sourcepath = new String[0]; private String[] sourcepath = new String[0];
private boolean resolveBindings = false; private boolean resolveBindings = false;
public Map<String, String> getProperties() { public Map<String, Map<String, String>> getProperties() {
return Collections.unmodifiableMap(properties); return Collections.unmodifiableMap(allProperties);
} }
public void setClasspath(List<String> classpath) { public void setClasspath(List<String> classpath) {
@@ -77,7 +77,7 @@ public class CodebaseContext {
private final Map<String, List<String>> interfaceToImpls = new HashMap<>(); private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
public void scan(Path rootDir, Set<String> customIgnorePatterns) throws IOException { public void scan(Path rootDir, Set<String> customIgnorePatterns) throws IOException {
this.properties = propertyResolver.resolveProperties(rootDir); this.allProperties = propertyResolver.resolveAllProperties(rootDir);
Set<String> activeIgnore = new HashSet<>(ignorePatterns); Set<String> activeIgnore = new HashSet<>(ignorePatterns);
activeIgnore.addAll(customIgnorePatterns); activeIgnore.addAll(customIgnorePatterns);
@@ -179,8 +179,7 @@ public class CodebaseContext {
if (mod instanceof Annotation ann) { if (mod instanceof Annotation ann) {
String name = ann.getTypeName().getFullyQualifiedName(); String name = ann.getTypeName().getFullyQualifiedName();
if (name.endsWith("Value")) { if (name.endsWith("Value")) {
String placeholder = extractAnnotationValue(ann); return extractAnnotationValue(ann);
return propertyResolver.resolveValue(placeholder, properties);
} }
} }
} }

View File

@@ -45,6 +45,9 @@ public class ExporterCommand implements Callable<Integer> {
@Option(names = {"--no-diamonds"}, description = "Render choice/junction states as regular states instead of diamonds.", negatable = true, defaultValue = "true") @Option(names = {"--no-diamonds"}, description = "Render choice/junction states as regular states instead of diamonds.", negatable = true, defaultValue = "true")
private boolean renderChoicesAsDiamonds; private boolean renderChoicesAsDiamonds;
@Option(names = {"-p", "--profiles"}, description = "Spring profiles to activate for property resolution.", split = ",")
private List<String> activeProfiles;
@Override @Override
public Integer call() throws Exception { public Integer call() throws Exception {
var out = spec.commandLine().getOut(); var out = spec.commandLine().getOut();
@@ -66,10 +69,11 @@ public class ExporterCommand implements Callable<Integer> {
} }
try { try {
List<String> profiles = activeProfiles != null ? activeProfiles : java.util.Collections.emptyList();
if (jsonFile != null) { if (jsonFile != null) {
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats); exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles);
} else { } else {
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds); exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles);
} }
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath())); out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath()));
return 0; return 0;

View File

@@ -97,12 +97,40 @@ public class ExportService {
} }
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats) throws IOException { public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats) throws IOException {
runJsonExporter(jsonFile, outputDir, selectedFormats, Collections.emptyList());
}
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats, List<String> activeProfiles) throws IOException {
JsonImportService jsonImportService = new JsonImportService(); JsonImportService jsonImportService = new JsonImportService();
AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile); AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile);
// Apply property resolution pass if placeholders exist
resolveProperties(result, activeProfiles);
generateOutputs(outputDir, result, selectedFormats); generateOutputs(outputDir, result, selectedFormats);
} }
private void resolveProperties(AnalysisResult result, List<String> activeProfiles) {
Map<String, Map<String, String>> allProps = result.getMetadata().getProperties();
if (allProps == null || allProps.isEmpty()) return;
// 1. Merge properties based on active profiles
Map<String, String> merged = new HashMap<>();
// Start with default
if (allProps.containsKey("default")) {
merged.putAll(allProps.get("default"));
}
// Overlay active profiles
for (String profile : activeProfiles) {
if (allProps.containsKey(profile)) {
merged.putAll(allProps.get(profile));
}
}
// 2. Delegate to result for resolution
result.applyResolution(merged);
}
private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, String machineFilter) throws IOException { private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, String machineFilter) throws IOException {
Set<String> processedLocations = new HashSet<>(); Set<String> processedLocations = new HashSet<>();

View File

@@ -0,0 +1,68 @@
package click.kamil.springstatemachineexporter.service;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Transition;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
public class DeferredProfileResolutionTest {
@Test
void shouldResolvePlaceholdersWithDefaultProfile() {
// Given
AnalysisResult result = createMockResult();
Map<String, String> properties = Map.of("app.state.initial", "START_RESOLVED");
// When
result.applyResolution(properties);
// Then
assertThat(result.getStartStates()).containsExactly("START_RESOLVED");
Transition t = result.getTransitions().get(0);
assertThat(t.getEvent()).isEqualTo("RESOLVED_EVENT");
assertThat(t.getSourceStates().get(0).fullIdentifier()).isEqualTo("START_RESOLVED");
}
@Test
void shouldResolveWithFallback() {
// Given
AnalysisResult result = AnalysisResult.builder()
.name("TestSM")
.startStates(Set.of("${missing.key:FALLBACK}"))
.transitions(Collections.emptyList())
.build();
// When
result.applyResolution(Collections.emptyMap());
// Then
assertThat(result.getStartStates()).containsExactly("FALLBACK");
}
private AnalysisResult createMockResult() {
String placeholder = "${app.state.initial}";
State s1 = State.of("START", placeholder);
State s2 = State.of("END", "END");
Transition t1 = new Transition();
t1.setEvent("${app.event.name:RESOLVED_EVENT}");
t1.setSourceStates(List.of(s1));
t1.setTargetStates(List.of(s2));
return AnalysisResult.builder()
.name("MockSM")
.startStates(Set.of(placeholder))
.endStates(Set.of("END"))
.states(Set.of(s1, s2))
.transitions(List.of(t1))
.build();
}
}

View File

@@ -0,0 +1,387 @@
{
"metadata" : {
"triggers" : [ {
"event" : "AUDIT_EVENT",
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
"methodName" : "preHandle",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 17
}, {
"event" : "PLACE_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
"methodName" : "place",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 16
}, {
"event" : "CANCEL_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
"methodName" : "cancel",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 21
}, {
"event" : "PAY_ORDER",
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
"methodName" : "processPayment",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 18
}, {
"event" : "SHIP_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
"methodName" : "onShippingReady",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 17
}, {
"event" : "RETURN_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
"methodName" : "onReturn",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 17
} ],
"entryPoints" : [ {
"type" : "REST",
"name" : "POST /api/enterprise/orders/place",
"className" : "click.kamil.examples.enterprise.api.OrderController",
"methodName" : "placeOrder",
"metadata" : {
"path" : "/api/enterprise/orders/place",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/enterprise/orders/{id}/cancel",
"className" : "click.kamil.examples.enterprise.api.OrderController",
"methodName" : "cancelOrder",
"metadata" : {
"path" : "/api/enterprise/orders/{id}/cancel",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
}, {
"type" : "CUSTOM",
"name" : "INTERCEPTOR: SecurityInterceptor.preHandle",
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
"methodName" : "preHandle",
"metadata" : {
"interceptorType" : "Spring MVC Interceptor"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/enterprise/payments",
"className" : "click.kamil.examples.enterprise.api.ReactivePaymentController",
"methodName" : "pay",
"metadata" : {
"path" : "/api/enterprise/payments",
"verb" : "POST"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ "RequestBody" ]
} ]
}, {
"type" : "JMS",
"name" : "JMS: shipping.queue",
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
"methodName" : "onShippingReady",
"metadata" : {
"protocol" : "JMS",
"destination" : "shipping.queue"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ ]
} ]
}, {
"type" : "RABBIT",
"name" : "RABBIT: returns.queue",
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
"methodName" : "onReturn",
"metadata" : {
"protocol" : "RABBIT",
"destination" : "returns.queue"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ ]
} ]
} ],
"callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/enterprise/orders/place",
"className" : "click.kamil.examples.enterprise.api.OrderController",
"methodName" : "placeOrder",
"metadata" : {
"path" : "/api/enterprise/orders/place",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.placeOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.place" ],
"triggerPoint" : {
"event" : "PLACE_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
"methodName" : "place",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 16
}
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/enterprise/orders/{id}/cancel",
"className" : "click.kamil.examples.enterprise.api.OrderController",
"methodName" : "cancelOrder",
"metadata" : {
"path" : "/api/enterprise/orders/{id}/cancel",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.cancelOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.cancel" ],
"triggerPoint" : {
"event" : "CANCEL_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
"methodName" : "cancel",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 21
}
}, {
"entryPoint" : {
"type" : "CUSTOM",
"name" : "INTERCEPTOR: SecurityInterceptor.preHandle",
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
"methodName" : "preHandle",
"metadata" : {
"interceptorType" : "Spring MVC Interceptor"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.enterprise.api.SecurityInterceptor.preHandle" ],
"triggerPoint" : {
"event" : "AUDIT_EVENT",
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
"methodName" : "preHandle",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 17
}
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/enterprise/payments",
"className" : "click.kamil.examples.enterprise.api.ReactivePaymentController",
"methodName" : "pay",
"metadata" : {
"path" : "/api/enterprise/payments",
"verb" : "POST"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ "RequestBody" ]
} ]
},
"methodChain" : [ "click.kamil.examples.enterprise.api.ReactivePaymentController.pay", "click.kamil.examples.enterprise.service.ReactivePaymentService.processPayment" ],
"triggerPoint" : {
"event" : "PAY_ORDER",
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
"methodName" : "processPayment",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 18
}
}, {
"entryPoint" : {
"type" : "JMS",
"name" : "JMS: shipping.queue",
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
"methodName" : "onShippingReady",
"metadata" : {
"protocol" : "JMS",
"destination" : "shipping.queue"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ ]
} ]
},
"methodChain" : [ "click.kamil.examples.enterprise.messaging.ShippingJmsListener.onShippingReady" ],
"triggerPoint" : {
"event" : "SHIP_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
"methodName" : "onShippingReady",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 17
}
}, {
"entryPoint" : {
"type" : "RABBIT",
"name" : "RABBIT: returns.queue",
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
"methodName" : "onReturn",
"metadata" : {
"protocol" : "RABBIT",
"destination" : "returns.queue"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ ]
} ]
},
"methodChain" : [ "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener.onReturn" ],
"triggerPoint" : {
"event" : "RETURN_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
"methodName" : "onReturn",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 17
}
} ],
"properties" : { }
},
"name" : "click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig",
"renderChoicesAsDiamonds" : false,
"startStates" : [ "NEW" ],
"transitions" : [ {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"NEW\"",
"fullIdentifier" : "NEW"
} ],
"targetStates" : [ {
"rawName" : "\"CHECK_AVAILABILITY\"",
"fullIdentifier" : "CHECK_AVAILABILITY"
} ],
"event" : "PLACE_ORDER",
"guard" : null,
"actions" : [ ],
"order" : null
}, {
"type" : "CHOICE",
"sourceStates" : [ {
"rawName" : "\"CHECK_AVAILABILITY\"",
"fullIdentifier" : "CHECK_AVAILABILITY"
} ],
"targetStates" : [ {
"rawName" : "\"PENDING_PAYMENT\"",
"fullIdentifier" : "PENDING_PAYMENT"
} ],
"event" : null,
"guard" : {
"expression" : "(c) -> true",
"isLambda" : true,
"internalLogic" : null
},
"actions" : [ ],
"order" : 0
}, {
"type" : "CHOICE",
"sourceStates" : [ {
"rawName" : "\"CHECK_AVAILABILITY\"",
"fullIdentifier" : "CHECK_AVAILABILITY"
} ],
"targetStates" : [ {
"rawName" : "\"CANCELLED\"",
"fullIdentifier" : "CANCELLED"
} ],
"event" : null,
"guard" : null,
"actions" : [ ],
"order" : 1
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"PENDING_PAYMENT\"",
"fullIdentifier" : "PENDING_PAYMENT"
} ],
"targetStates" : [ {
"rawName" : "\"PAID\"",
"fullIdentifier" : "PAID"
} ],
"event" : "PAY_ORDER",
"guard" : null,
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"PAID\"",
"fullIdentifier" : "PAID"
} ],
"targetStates" : [ {
"rawName" : "\"SHIPPED\"",
"fullIdentifier" : "SHIPPED"
} ],
"event" : "SHIP_ORDER",
"guard" : null,
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"SHIPPED\"",
"fullIdentifier" : "SHIPPED"
} ],
"targetStates" : [ {
"rawName" : "\"DELIVERED\"",
"fullIdentifier" : "DELIVERED"
} ],
"event" : "FINALIZE",
"guard" : null,
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"PAID\"",
"fullIdentifier" : "PAID"
} ],
"targetStates" : [ {
"rawName" : "\"CANCELLED\"",
"fullIdentifier" : "CANCELLED"
} ],
"event" : "CANCEL_ORDER",
"guard" : null,
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"DELIVERED\"",
"fullIdentifier" : "DELIVERED"
} ],
"targetStates" : [ {
"rawName" : "\"RETURNED\"",
"fullIdentifier" : "RETURNED"
} ],
"event" : "RETURN_ORDER",
"guard" : null,
"actions" : [ ],
"order" : null
} ],
"endStates" : [ "CANCELLED", "DELIVERED", "RETURNED" ]
}

View File

@@ -35,6 +35,9 @@ public class HtmlExporterCommand implements Callable<Integer> {
@Option(names = {"-i", "--input"}, description = "Input directory containing the Spring Boot project source code.") @Option(names = {"-i", "--input"}, description = "Input directory containing the Spring Boot project source code.")
private Path inputDir; private Path inputDir;
@Option(names = {"-j", "--json"}, description = "Input JSON file containing the state machine definition.")
private Path jsonFile;
@Option(names = {"-o", "--output"}, description = "Output directory for the generated portal.", defaultValue = "./out_html") @Option(names = {"-o", "--output"}, description = "Output directory for the generated portal.", defaultValue = "./out_html")
private Path outputDir; private Path outputDir;
@@ -58,29 +61,29 @@ public class HtmlExporterCommand implements Callable<Integer> {
var out = spec.commandLine().getOut(); var out = spec.commandLine().getOut();
var err = spec.commandLine().getErr(); var err = spec.commandLine().getErr();
if (inputDir == null) { if (inputDir == null && jsonFile == null) {
inputDir = Path.of("."); inputDir = Path.of(".");
} }
out.println(CommandLine.Help.Ansi.AUTO.string("@|bold,red Initializing Interactive Portal Generation...|@")); out.println(CommandLine.Help.Ansi.AUTO.string("@|bold,red Initializing Interactive Portal Generation...|@"));
if (jsonFile != null) {
out.println(CommandLine.Help.Ansi.AUTO.string("@|yellow Input JSON File:|@ " + jsonFile.toAbsolutePath()));
} else {
out.println(CommandLine.Help.Ansi.AUTO.string("@|yellow Input Directory:|@ " + inputDir.toAbsolutePath())); out.println(CommandLine.Help.Ansi.AUTO.string("@|yellow Input Directory:|@ " + inputDir.toAbsolutePath()));
}
out.println(CommandLine.Help.Ansi.AUTO.string("@|yellow Output Directory:|@ " + outputDir.toAbsolutePath())); out.println(CommandLine.Help.Ansi.AUTO.string("@|yellow Output Directory:|@ " + outputDir.toAbsolutePath()));
if (flowsFile != null) {
out.println(CommandLine.Help.Ansi.AUTO.string("@|yellow Custom Flows:|@ " + flowsFile.toAbsolutePath()));
}
if (machineFilter != null) {
out.println(CommandLine.Help.Ansi.AUTO.string("@|yellow Machine Filter:|@ " + machineFilter));
}
try { try {
// Force HTML and JSON formats for the interactive portal // Force HTML and JSON formats for the interactive portal
List<String> formats = List.of("html", "json"); List<String> formats = List.of("html", "json");
List<String> profiles = activeProfiles != null ? activeProfiles : java.util.Collections.emptyList();
if (jsonFile != null) {
exportService.runJsonExporter(jsonFile, outputDir, formats, profiles);
} else {
boolean renderChoicesAsDiamonds = !noDiamonds; boolean renderChoicesAsDiamonds = !noDiamonds;
exportService.runExporter(inputDir, outputDir, formats, renderChoicesAsDiamonds, profiles, flowsFile, machineFilter);
exportService.runExporter(inputDir, outputDir, formats, renderChoicesAsDiamonds, }
activeProfiles != null ? activeProfiles : java.util.Collections.emptyList(),
flowsFile, machineFilter);
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ Interactive documentation generated successfully.")); out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ Interactive documentation generated successfully."));