v2.4 extended analysis render update
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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<>();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -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" ]
|
||||||
|
}
|
||||||
@@ -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...|@"));
|
||||||
out.println(CommandLine.Help.Ansi.AUTO.string("@|yellow Input Directory:|@ " + inputDir.toAbsolutePath()));
|
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 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();
|
||||||
|
|
||||||
boolean renderChoicesAsDiamonds = !noDiamonds;
|
if (jsonFile != null) {
|
||||||
|
exportService.runJsonExporter(jsonFile, outputDir, formats, profiles);
|
||||||
exportService.runExporter(inputDir, outputDir, formats, renderChoicesAsDiamonds,
|
} else {
|
||||||
activeProfiles != null ? activeProfiles : java.util.Collections.emptyList(),
|
boolean renderChoicesAsDiamonds = !noDiamonds;
|
||||||
flowsFile, machineFilter);
|
exportService.runExporter(inputDir, outputDir, formats, renderChoicesAsDiamonds, profiles, 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."));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user