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) {
|
||||
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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user