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) {
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()
.properties(properties)

View File

@@ -6,31 +6,96 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import click.kamil.springstatemachineexporter.model.Transition;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.jackson.Jacksonized;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Getter
@Builder
@Setter
@Builder(toBuilder = true)
@Jacksonized
@JsonIgnoreProperties(ignoreUnknown = true)
public class AnalysisResult {
private final String name;
private String name;
@Builder.Default
private final Set<click.kamil.springstatemachineexporter.model.State> states = java.util.Collections.emptySet();
private final List<Transition> transitions;
private final Set<String> startStates;
private final Set<String> endStates;
private final boolean renderChoicesAsDiamonds;
private Set<click.kamil.springstatemachineexporter.model.State> states = java.util.Collections.emptySet();
private List<Transition> transitions;
private Set<String> startStates;
private Set<String> endStates;
private boolean renderChoicesAsDiamonds;
@Builder.Default
private final List<BusinessFlow> flows = java.util.Collections.emptyList();
private List<BusinessFlow> flows = java.util.Collections.emptyList();
@Builder.Default
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) {
if (newMetadata == null) return;
@@ -43,7 +108,7 @@ public class AnalysisResult {
List<CallChain> combinedCallChains = new java.util.ArrayList<>(this.metadata.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());
this.metadata = CodebaseMetadata.builder()

View File

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

View File

@@ -26,10 +26,10 @@ public class EntryPoint {
}
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 methodName;
private final Map<String, String> metadata; // e.g., path, topic, exchange
private Map<String, String> metadata; // e.g., path, topic, exchange
@Builder.Default
private final List<Parameter> parameters = java.util.Collections.emptyList();
}

View File

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

View File

@@ -42,6 +42,37 @@ public class PropertyResolver {
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) {
try (Stream<Path> paths = Files.walk(rootDir)) {
paths.filter(p -> Files.isRegularFile(p))

View File

@@ -24,7 +24,7 @@ public interface CodebaseIntelligenceProvider {
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
public Map<String, String> resolveProperties() {
public Map<String, Map<String, String>> resolveProperties() {
return context.getProperties();
}
}

View File

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

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")
private boolean renderChoicesAsDiamonds;
@Option(names = {"-p", "--profiles"}, description = "Spring profiles to activate for property resolution.", split = ",")
private List<String> activeProfiles;
@Override
public Integer call() throws Exception {
var out = spec.commandLine().getOut();
@@ -66,10 +69,11 @@ public class ExporterCommand implements Callable<Integer> {
}
try {
List<String> profiles = activeProfiles != null ? activeProfiles : java.util.Collections.emptyList();
if (jsonFile != null) {
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats);
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles);
} 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()));
return 0;

View File

@@ -97,12 +97,40 @@ public class ExportService {
}
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();
AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile);
// Apply property resolution pass if placeholders exist
resolveProperties(result, activeProfiles);
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 {
Set<String> processedLocations = new HashSet<>();