This commit is contained in:
2026-06-17 06:31:11 +02:00
parent 79ddb307c1
commit 1f4a1667c3
27 changed files with 2251 additions and 32 deletions

View File

@@ -10,8 +10,10 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Stream;
@Slf4j
public class PropertyResolver {
@@ -42,32 +44,33 @@ public class PropertyResolver {
return allProperties;
}
public Map<String, Map<String, String>> resolveAllProperties(Path rootDir) {
public Map<String, Map<String, String>> resolveAllProperties(Set<Path> rootDirs) {
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);
for (Path rootDir : rootDirs) {
// 1. Load default profile
Map<String, String> defaultProps = profiles.computeIfAbsent("default", k -> new HashMap<>());
loadMatching(rootDir, "application.properties", defaultProps);
loadMatching(rootDir, "application.yml", defaultProps);
loadMatching(rootDir, "application.yaml", 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);
// 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;

View File

@@ -0,0 +1,140 @@
package click.kamil.springstatemachineexporter.analysis.resolver;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
public class SiblingDependencyResolver {
private static final Pattern GRADLE_PROJECT_DEP = Pattern.compile("project\\(['\"]:(.*?)['\"]\\)");
private static final Pattern MAVEN_DEPENDENCY = Pattern.compile("<dependency>\\s*<groupId>(.*?)</groupId>\\s*<artifactId>(.*?)</artifactId>\\s*<version>(.*?)</version>", Pattern.DOTALL);
public Set<Path> resolveSiblings(Path projectDir) {
Set<Path> siblings = new HashSet<>();
try {
siblings.addAll(resolveGradleSiblings(projectDir));
siblings.addAll(resolveMavenSiblings(projectDir));
} catch (Exception e) {
log.warn("Error resolving sibling dependencies for {}", projectDir, e);
}
return siblings;
}
private Set<Path> resolveGradleSiblings(Path projectDir) throws IOException {
Set<Path> siblings = new HashSet<>();
Path buildGradle = projectDir.resolve("build.gradle");
if (!Files.exists(buildGradle)) {
buildGradle = projectDir.resolve("build.gradle.kts");
}
if (Files.exists(buildGradle)) {
log.info("Found build.gradle at {}", buildGradle);
String content = Files.readString(buildGradle);
Matcher matcher = GRADLE_PROJECT_DEP.matcher(content);
Set<String> gradlePaths = new HashSet<>();
while (matcher.find()) {
gradlePaths.add(matcher.group(1));
}
log.info("Found project dependencies: {}", gradlePaths);
if (!gradlePaths.isEmpty()) {
Path rootDir = findGradleRoot(projectDir);
log.info("Resolved Gradle root to: {}", rootDir);
if (rootDir != null) {
for (String gradlePath : gradlePaths) {
String relativePath = gradlePath.replace(':', '/');
Path resolvedSibling = rootDir.resolve(relativePath).normalize();
if (Files.exists(resolvedSibling)) {
log.info("Discovered local Gradle sibling dependency: {}", resolvedSibling);
siblings.add(resolvedSibling);
}
}
}
}
}
return siblings;
}
private Path findGradleRoot(Path start) {
Path current = start.toAbsolutePath();
Path lastSettingsDir = null;
while (current != null) {
if (Files.exists(current.resolve("settings.gradle")) || Files.exists(current.resolve("settings.gradle.kts"))) {
lastSettingsDir = current;
}
if (Files.exists(current.resolve(".git"))) {
return lastSettingsDir != null ? lastSettingsDir : current;
}
current = current.getParent();
}
return lastSettingsDir;
}
private Set<Path> resolveMavenSiblings(Path projectDir) throws IOException {
Set<Path> siblings = new HashSet<>();
Path pom = projectDir.resolve("pom.xml");
if (Files.exists(pom)) {
String content = Files.readString(pom);
Matcher matcher = MAVEN_DEPENDENCY.matcher(content);
Set<String> targetArtifacts = new HashSet<>();
while (matcher.find()) {
targetArtifacts.add(matcher.group(2)); // Just tracking artifactId for simplicity
}
if (!targetArtifacts.isEmpty()) {
Path rootDir = findMavenRoot(projectDir);
if (rootDir != null) {
// Find all pom.xml files in the workspace (excluding node_modules, build, etc.)
try (Stream<Path> paths = Files.walk(rootDir)) {
paths.filter(p -> p.getFileName().toString().equals("pom.xml")
&& !p.toString().contains("/target/")
&& !p.toString().contains("/node_modules/"))
.forEach(p -> {
try {
String pContent = Files.readString(p);
Matcher artMatcher = Pattern.compile("<artifactId>(.*?)</artifactId>").matcher(pContent);
if (artMatcher.find()) {
String artifactId = artMatcher.group(1);
if (targetArtifacts.contains(artifactId)) {
Path siblingDir = p.getParent();
// Don't add ourselves
if (!siblingDir.equals(projectDir)) {
log.info("Discovered local Maven sibling dependency: {}", siblingDir);
siblings.add(siblingDir);
}
}
}
} catch (IOException e) {
// ignore
}
});
}
}
}
}
return siblings;
}
private Path findMavenRoot(Path start) {
Path current = start.toAbsolutePath();
Path lastPomDir = null;
while (current != null) {
if (Files.exists(current.resolve("pom.xml"))) {
lastPomDir = current;
}
if (Files.exists(current.resolve(".git"))) {
return lastPomDir != null ? lastPomDir : current;
}
current = current.getParent();
}
return lastPomDir;
}
}

View File

@@ -36,7 +36,7 @@ public class SpringMvcDetector {
for (Object modifier : node.modifiers()) {
if (modifier instanceof Annotation annotation) {
String name = annotation.getTypeName().getFullyQualifiedName();
if (name.endsWith("RestController") || name.endsWith("Controller") || name.endsWith("Path")) {
if (name.endsWith("RestController") || name.endsWith("Controller") || name.endsWith("Path") || name.endsWith("RequestMapping")) {
return true;
}
}

View File

@@ -29,7 +29,9 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AstTransitionParser {
private static final ConstantResolver constantResolver = new ConstantResolver();
@@ -258,6 +260,21 @@ public class AstTransitionParser {
if (expr instanceof MethodInvocation mi) {
IMethodBinding binding = mi.resolveMethodBinding();
if (binding != null) {
// Try to resolve deeper logic from the return type (e.g., a Bean returning an Action interface)
ITypeBinding returnType = binding.getReturnType();
if (returnType != null) {
String returnFqn = returnType.getTypeDeclaration().getQualifiedName();
TypeDeclaration returnTd = context.getTypeDeclaration(returnFqn);
if (returnTd != null) {
for (MethodDeclaration rmd : returnTd.getMethods()) {
String name = rmd.getName().getIdentifier();
if (name.equals("execute") || name.equals("evaluate")) {
return rmd.toString();
}
}
}
}
// Try same file
ASTNode declNode = cu.findDeclaringNode(binding.getKey());
if (declNode instanceof MethodDeclaration md) {
@@ -267,7 +284,7 @@ public class AstTransitionParser {
// Try other files via CodebaseContext
ITypeBinding declaringClass = binding.getDeclaringClass();
if (declaringClass != null) {
String fqn = declaringClass.getQualifiedName();
String fqn = declaringClass.getTypeDeclaration().getQualifiedName();
TypeDeclaration td = context.getTypeDeclaration(fqn);
if (td != null) {
for (MethodDeclaration md : td.getMethods()) {
@@ -287,7 +304,7 @@ public class AstTransitionParser {
if (binding != null) {
ITypeBinding declaringClass = binding.getDeclaringClass();
if (declaringClass != null) {
String fqn = declaringClass.getQualifiedName();
String fqn = declaringClass.getTypeDeclaration().getQualifiedName();
TypeDeclaration td = context.getTypeDeclaration(fqn);
if (td != null) {
for (MethodDeclaration md : td.getMethods()) {

View File

@@ -69,20 +69,20 @@ public class CodebaseContext {
}
public void scan(Path rootDir) throws IOException {
scan(rootDir, Collections.emptySet());
scan(Set.of(rootDir), Collections.emptySet());
}
private final List<CompilationUnit> allCus = new ArrayList<>();
private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
public void scan(Path rootDir, Set<String> customIgnorePatterns) throws IOException {
this.allProperties = propertyResolver.resolveAllProperties(rootDir);
public void scan(Set<Path> rootDirs, Set<String> customIgnorePatterns) throws IOException {
this.allProperties = propertyResolver.resolveAllProperties(rootDirs);
Set<String> activeIgnore = new HashSet<>(ignorePatterns);
activeIgnore.addAll(customIgnorePatterns);
List<Path> javaFiles = FileUtils.findFilesWithExtension(Set.of(rootDir), ".java", activeIgnore);
List<Path> javaFiles = FileUtils.findFilesWithExtension(rootDirs, ".java", activeIgnore);
for (Path javaFile : javaFiles) {
String source = Files.readString(javaFile);
CompilationUnit cu = parse(source, javaFile.getFileName().toString());

View File

@@ -26,6 +26,7 @@ import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
public class ExportService {
@@ -61,7 +62,16 @@ public class ExportService {
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter) throws IOException {
CodebaseContext context = new CodebaseContext();
context.setActiveProfiles(activeProfiles);
context.setSourcepath(List.of(inputDir.toString()));
click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver siblingResolver = new click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver();
Set<Path> allPaths = new HashSet<>();
allPaths.add(inputDir);
allPaths.addAll(siblingResolver.resolveSiblings(inputDir));
context.setSourcepath(allPaths.stream()
.map(p -> p.resolve("src/main/java").toString())
.filter(p -> Files.exists(Path.of(p)))
.collect(Collectors.toList()));
context.setResolveBindings(true);
Path hintsFile = inputDir.resolve("hints.json");
@@ -90,7 +100,7 @@ public class ExportService {
}
}
context.scan(inputDir);
context.scan(allPaths, Collections.emptySet());
CodebaseIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, inputDir);
exportAll(context, intelligence, outputDir, selectedFormats, renderChoicesAsDiamonds, flows, machineFilter, activeProfiles != null ? activeProfiles : Collections.emptyList());