From 1f4a1667c3981a5bb815596b5840f41c020ce7c7 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Wed, 17 Jun 2026 06:31:11 +0200 Subject: [PATCH] multi --- ...il.multi.core.OrderStateMachineConfig.html | 724 ++++++++++++++++++ ...il.multi.core.OrderStateMachineConfig.json | 108 +++ ...il.multi.core.OrderStateMachineConfig.puml | 32 + settings.gradle | 2 + .../analysis/resolver/PropertyResolver.java | 49 +- .../resolver/SiblingDependencyResolver.java | 140 ++++ .../analysis/service/SpringMvcDetector.java | 2 +- .../ast/app/AstTransitionParser.java | 21 +- .../ast/common/CodebaseContext.java | 8 +- .../service/ExportService.java | 14 +- .../RegressionTest.java | 6 + .../EnterpriseStateMachineConfig.json | 24 + .../InheritanceStateMachineConfig.json | 10 + .../OrderStateMachineConfig.dot | 12 + .../OrderStateMachineConfig.json | 108 +++ .../OrderStateMachineConfig.puml | 32 + .../OrderStateMachineConfig.scxml.xml | 12 + ...il.multi.core.OrderStateMachineConfig.html | 724 ++++++++++++++++++ ...il.multi.core.OrderStateMachineConfig.json | 108 +++ ...il.multi.core.OrderStateMachineConfig.puml | 32 + .../api-module/build.gradle | 12 + .../java/click/kamil/multi/api/OrderApi.java | 12 + .../click/kamil/multi/api/ProcessAction.java | 12 + .../core-module/build.gradle | 13 + .../kamil/multi/core/OrderController.java | 20 + .../multi/core/OrderStateMachineConfig.java | 43 ++ .../multi_module_sample/settings.gradle | 3 + 27 files changed, 2251 insertions(+), 32 deletions(-) create mode 100644 out_multi/click.kamil.multi.core.OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.html create mode 100644 out_multi/click.kamil.multi.core.OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.json create mode 100644 out_multi/click.kamil.multi.core.OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.puml create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/SiblingDependencyResolver.java create mode 100644 state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.dot create mode 100644 state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.json create mode 100644 state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.puml create mode 100644 state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.scxml.xml create mode 100644 state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.html create mode 100644 state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.json create mode 100644 state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.puml create mode 100644 state_machines/multi_module_sample/api-module/build.gradle create mode 100644 state_machines/multi_module_sample/api-module/src/main/java/click/kamil/multi/api/OrderApi.java create mode 100644 state_machines/multi_module_sample/api-module/src/main/java/click/kamil/multi/api/ProcessAction.java create mode 100644 state_machines/multi_module_sample/core-module/build.gradle create mode 100644 state_machines/multi_module_sample/core-module/src/main/java/click/kamil/multi/core/OrderController.java create mode 100644 state_machines/multi_module_sample/core-module/src/main/java/click/kamil/multi/core/OrderStateMachineConfig.java create mode 100644 state_machines/multi_module_sample/settings.gradle diff --git a/out_multi/click.kamil.multi.core.OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.html b/out_multi/click.kamil.multi.core.OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.html new file mode 100644 index 0000000..4bf226e --- /dev/null +++ b/out_multi/click.kamil.multi.core.OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.html @@ -0,0 +1,724 @@ + + + + + + State Machine Explorer - click.kamil.multi.core.OrderStateMachineConfig + + + + + + + + + + + + + + + + +
+ +
+ NEWPROCESSINGCOMPLETEDSUBMIT / processAction()FINISH +
+
+ + + + + + diff --git a/out_multi/click.kamil.multi.core.OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.json b/out_multi/click.kamil.multi.core.OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.json new file mode 100644 index 0000000..b0c7a90 --- /dev/null +++ b/out_multi/click.kamil.multi.core.OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.json @@ -0,0 +1,108 @@ +{ + "metadata" : { + "triggers" : [ { + "event" : "SUBMIT", + "className" : "click.kamil.multi.core.OrderController", + "methodName" : "submitOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 18 + } ], + "entryPoints" : [ { + "type" : "REST", + "name" : "POST /orders/submit", + "className" : "click.kamil.multi.core.OrderController", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/orders/submit", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "orderId", + "type" : "String", + "annotations" : [ "RequestBody" ] + } ] + }, { + "type" : "REST", + "name" : "POST /orders/submit", + "className" : "click.kamil.multi.api.OrderApi", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/orders/submit", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "orderId", + "type" : "String", + "annotations" : [ "RequestBody" ] + } ] + } ], + "callChains" : [ { + "entryPoint" : { + "type" : "REST", + "name" : "POST /orders/submit", + "className" : "click.kamil.multi.core.OrderController", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/orders/submit", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "orderId", + "type" : "String", + "annotations" : [ "RequestBody" ] + } ] + }, + "methodChain" : [ "click.kamil.multi.core.OrderController.submitOrder" ], + "triggerPoint" : { + "event" : "SUBMIT", + "className" : "click.kamil.multi.core.OrderController", + "methodName" : "submitOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 18 + } + } ], + "properties" : { + "default" : { } + } + }, + "name" : "click.kamil.multi.core.OrderStateMachineConfig", + "renderChoicesAsDiamonds" : true, + "startStates" : [ "NEW" ], + "transitions" : [ { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"NEW\"", + "fullIdentifier" : "NEW" + } ], + "targetStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "event" : "SUBMIT", + "guard" : null, + "actions" : [ { + "expression" : "processAction()", + "isLambda" : false, + "internalLogic" : "@Override default void execute(StateContext context){\n System.out.println(\"Processing order: \" + context.getMessageHeader(\"orderId\"));\n}\n", + "lineNumber" : 31 + } ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "targetStates" : [ { + "rawName" : "\"COMPLETED\"", + "fullIdentifier" : "COMPLETED" + } ], + "event" : "FINISH", + "guard" : null, + "actions" : [ ], + "order" : null + } ], + "endStates" : [ "COMPLETED" ] +} diff --git a/out_multi/click.kamil.multi.core.OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.puml b/out_multi/click.kamil.multi.core.OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.puml new file mode 100644 index 0000000..3b59d0b --- /dev/null +++ b/out_multi/click.kamil.multi.core.OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.puml @@ -0,0 +1,32 @@ +@startuml +!pragma layout smetana +set separator none +hide empty description +hide stereotype +skinparam state { + BackgroundColor white + BorderColor #94a3b8 + BorderThickness 1 + FontName Inter + FontSize 9 + FontStyle bold + RoundCorner 20 + Padding 1 +} +skinparam shadowing false +skinparam ArrowFontName JetBrains Mono +skinparam ArrowFontSize 8 +skinparam ArrowColor #cbd5e1 +skinparam ArrowThickness 1 +skinparam dpi 110 +skinparam svgLinkTarget _self + +[*] --> NEW + + +NEW -[#1E90FF,bold]-> PROCESSING <> <> : SUBMIT / processAction() +PROCESSING -[#1E90FF,bold]-> COMPLETED <> <> : FINISH + +COMPLETED --> [*] +@enduml + diff --git a/settings.gradle b/settings.gradle index 7e272bc..7bf430a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,3 +17,5 @@ include ':state_machines:extended_analysis_sample' include ':state_machines:inheritance_sample' include ':state_machines:ultimate_ecosystem_sm' include ':state_machines:enterprise_order_system' +include ':state_machines:multi_module_sample:api-module' +include ':state_machines:multi_module_sample:core-module' diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PropertyResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PropertyResolver.java index 65db461..cd590f0 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PropertyResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PropertyResolver.java @@ -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> resolveAllProperties(Path rootDir) { + public Map> resolveAllProperties(Set rootDirs) { Map> profiles = new HashMap<>(); - // 1. Load default profile - Map 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 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 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 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 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 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; diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/SiblingDependencyResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/SiblingDependencyResolver.java new file mode 100644 index 0000000..f0a7d59 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/SiblingDependencyResolver.java @@ -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("\\s*(.*?)\\s*(.*?)\\s*(.*?)", Pattern.DOTALL); + + public Set resolveSiblings(Path projectDir) { + Set 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 resolveGradleSiblings(Path projectDir) throws IOException { + Set 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 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 resolveMavenSiblings(Path projectDir) throws IOException { + Set 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 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 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("(.*?)").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; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringMvcDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringMvcDetector.java index 62128df..0e53b8c 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringMvcDetector.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringMvcDetector.java @@ -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; } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java index a3260d6..5d21fa8 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java @@ -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()) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java index 5d115c1..cf01481 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java @@ -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 allCus = new ArrayList<>(); private final Map> interfaceToImpls = new HashMap<>(); - public void scan(Path rootDir, Set customIgnorePatterns) throws IOException { - this.allProperties = propertyResolver.resolveAllProperties(rootDir); + public void scan(Set rootDirs, Set customIgnorePatterns) throws IOException { + this.allProperties = propertyResolver.resolveAllProperties(rootDirs); Set activeIgnore = new HashSet<>(ignorePatterns); activeIgnore.addAll(customIgnorePatterns); - List javaFiles = FileUtils.findFilesWithExtension(Set.of(rootDir), ".java", activeIgnore); + List javaFiles = FileUtils.findFilesWithExtension(rootDirs, ".java", activeIgnore); for (Path javaFile : javaFiles) { String source = Files.readString(javaFile); CompilationUnit cu = parse(source, javaFile.getFileName().toString()); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java index 28424ad..cb7422d 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java @@ -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 selectedFormats, boolean renderChoicesAsDiamonds, List 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 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()); diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java index 4f189f6..d37af92 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java @@ -125,6 +125,12 @@ public class RegressionTest { root.resolve("state_machines/enterprise_order_system"), Path.of("src/test/resources/golden/EnterpriseStateMachineConfig"), "EnterpriseStateMachineConfig" + ), + new TestScenario( + "Multi-Module Sample", + root.resolve("state_machines/multi_module_sample/core-module"), + Path.of("src/test/resources/golden/OrderStateMachineConfig"), + "OrderStateMachineConfig" ) ); } diff --git a/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json index 74e2d02..0f23217 100644 --- a/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json @@ -44,6 +44,30 @@ "lineNumber" : 17 } ], "entryPoints" : [ { + "type" : "REST", + "name" : "POST /api/enterprise/orders/place", + "className" : "click.kamil.examples.enterprise.api.OrderApi", + "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.OrderApi", + "methodName" : "cancelOrder", + "metadata" : { + "path" : "/api/enterprise/orders/{id}/cancel", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "id", + "type" : "String", + "annotations" : [ "PathVariable" ] + } ] + }, { "type" : "REST", "name" : "POST /api/enterprise/orders/place", "className" : "click.kamil.examples.enterprise.api.OrderController", diff --git a/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json index d9a7446..118d5b0 100644 --- a/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json @@ -9,6 +9,16 @@ "lineNumber" : 15 } ], "entryPoints" : [ { + "type" : "REST", + "name" : "POST /api/v2/orders/submit", + "className" : "click.kamil.examples.statemachine.inheritance.api.OrderApi", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/api/v2/orders/submit", + "verb" : "POST" + }, + "parameters" : [ ] + }, { "type" : "REST", "name" : "POST /api/v2/orders/submit", "className" : "click.kamil.examples.statemachine.inheritance.api.OrderControllerImpl", diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.dot b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.dot new file mode 100644 index 0000000..b515a49 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.dot @@ -0,0 +1,12 @@ +digraph statemachine { + rankdir=LR; + node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"]; + edge [fontname="Arial", fontsize=10]; + + _start [shape=circle, label="", fillcolor=black, width=0.1]; + _start -> NEW; + COMPLETED [fillcolor=lightgray]; + NEW -> PROCESSING [label="SUBMIT / processAction()", style="solid", color="black"]; + PROCESSING -> COMPLETED [label="FINISH", style="solid", color="black"]; +} + diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.json new file mode 100644 index 0000000..b0c7a90 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.json @@ -0,0 +1,108 @@ +{ + "metadata" : { + "triggers" : [ { + "event" : "SUBMIT", + "className" : "click.kamil.multi.core.OrderController", + "methodName" : "submitOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 18 + } ], + "entryPoints" : [ { + "type" : "REST", + "name" : "POST /orders/submit", + "className" : "click.kamil.multi.core.OrderController", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/orders/submit", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "orderId", + "type" : "String", + "annotations" : [ "RequestBody" ] + } ] + }, { + "type" : "REST", + "name" : "POST /orders/submit", + "className" : "click.kamil.multi.api.OrderApi", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/orders/submit", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "orderId", + "type" : "String", + "annotations" : [ "RequestBody" ] + } ] + } ], + "callChains" : [ { + "entryPoint" : { + "type" : "REST", + "name" : "POST /orders/submit", + "className" : "click.kamil.multi.core.OrderController", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/orders/submit", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "orderId", + "type" : "String", + "annotations" : [ "RequestBody" ] + } ] + }, + "methodChain" : [ "click.kamil.multi.core.OrderController.submitOrder" ], + "triggerPoint" : { + "event" : "SUBMIT", + "className" : "click.kamil.multi.core.OrderController", + "methodName" : "submitOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 18 + } + } ], + "properties" : { + "default" : { } + } + }, + "name" : "click.kamil.multi.core.OrderStateMachineConfig", + "renderChoicesAsDiamonds" : true, + "startStates" : [ "NEW" ], + "transitions" : [ { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"NEW\"", + "fullIdentifier" : "NEW" + } ], + "targetStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "event" : "SUBMIT", + "guard" : null, + "actions" : [ { + "expression" : "processAction()", + "isLambda" : false, + "internalLogic" : "@Override default void execute(StateContext context){\n System.out.println(\"Processing order: \" + context.getMessageHeader(\"orderId\"));\n}\n", + "lineNumber" : 31 + } ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "targetStates" : [ { + "rawName" : "\"COMPLETED\"", + "fullIdentifier" : "COMPLETED" + } ], + "event" : "FINISH", + "guard" : null, + "actions" : [ ], + "order" : null + } ], + "endStates" : [ "COMPLETED" ] +} diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.puml b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.puml new file mode 100644 index 0000000..3b59d0b --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.puml @@ -0,0 +1,32 @@ +@startuml +!pragma layout smetana +set separator none +hide empty description +hide stereotype +skinparam state { + BackgroundColor white + BorderColor #94a3b8 + BorderThickness 1 + FontName Inter + FontSize 9 + FontStyle bold + RoundCorner 20 + Padding 1 +} +skinparam shadowing false +skinparam ArrowFontName JetBrains Mono +skinparam ArrowFontSize 8 +skinparam ArrowColor #cbd5e1 +skinparam ArrowThickness 1 +skinparam dpi 110 +skinparam svgLinkTarget _self + +[*] --> NEW + + +NEW -[#1E90FF,bold]-> PROCESSING <> <> : SUBMIT / processAction() +PROCESSING -[#1E90FF,bold]-> COMPLETED <> <> : FINISH + +COMPLETED --> [*] +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.scxml.xml b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.scxml.xml new file mode 100644 index 0000000..6549088 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/OrderStateMachineConfig.scxml.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.html b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.html new file mode 100644 index 0000000..4bf226e --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.html @@ -0,0 +1,724 @@ + + + + + + State Machine Explorer - click.kamil.multi.core.OrderStateMachineConfig + + + + + + + + + + + + + + + + +
+ +
+ NEWPROCESSINGCOMPLETEDSUBMIT / processAction()FINISH +
+
+ + + + + + diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.json new file mode 100644 index 0000000..b0c7a90 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.json @@ -0,0 +1,108 @@ +{ + "metadata" : { + "triggers" : [ { + "event" : "SUBMIT", + "className" : "click.kamil.multi.core.OrderController", + "methodName" : "submitOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 18 + } ], + "entryPoints" : [ { + "type" : "REST", + "name" : "POST /orders/submit", + "className" : "click.kamil.multi.core.OrderController", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/orders/submit", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "orderId", + "type" : "String", + "annotations" : [ "RequestBody" ] + } ] + }, { + "type" : "REST", + "name" : "POST /orders/submit", + "className" : "click.kamil.multi.api.OrderApi", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/orders/submit", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "orderId", + "type" : "String", + "annotations" : [ "RequestBody" ] + } ] + } ], + "callChains" : [ { + "entryPoint" : { + "type" : "REST", + "name" : "POST /orders/submit", + "className" : "click.kamil.multi.core.OrderController", + "methodName" : "submitOrder", + "metadata" : { + "path" : "/orders/submit", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "orderId", + "type" : "String", + "annotations" : [ "RequestBody" ] + } ] + }, + "methodChain" : [ "click.kamil.multi.core.OrderController.submitOrder" ], + "triggerPoint" : { + "event" : "SUBMIT", + "className" : "click.kamil.multi.core.OrderController", + "methodName" : "submitOrder", + "sourceModule" : null, + "stateMachineId" : null, + "lineNumber" : 18 + } + } ], + "properties" : { + "default" : { } + } + }, + "name" : "click.kamil.multi.core.OrderStateMachineConfig", + "renderChoicesAsDiamonds" : true, + "startStates" : [ "NEW" ], + "transitions" : [ { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"NEW\"", + "fullIdentifier" : "NEW" + } ], + "targetStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "event" : "SUBMIT", + "guard" : null, + "actions" : [ { + "expression" : "processAction()", + "isLambda" : false, + "internalLogic" : "@Override default void execute(StateContext context){\n System.out.println(\"Processing order: \" + context.getMessageHeader(\"orderId\"));\n}\n", + "lineNumber" : 31 + } ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "\"PROCESSING\"", + "fullIdentifier" : "PROCESSING" + } ], + "targetStates" : [ { + "rawName" : "\"COMPLETED\"", + "fullIdentifier" : "COMPLETED" + } ], + "event" : "FINISH", + "guard" : null, + "actions" : [ ], + "order" : null + } ], + "endStates" : [ "COMPLETED" ] +} diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.puml b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.puml new file mode 100644 index 0000000..3b59d0b --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfig/click.kamil.multi.core.OrderStateMachineConfig.puml @@ -0,0 +1,32 @@ +@startuml +!pragma layout smetana +set separator none +hide empty description +hide stereotype +skinparam state { + BackgroundColor white + BorderColor #94a3b8 + BorderThickness 1 + FontName Inter + FontSize 9 + FontStyle bold + RoundCorner 20 + Padding 1 +} +skinparam shadowing false +skinparam ArrowFontName JetBrains Mono +skinparam ArrowFontSize 8 +skinparam ArrowColor #cbd5e1 +skinparam ArrowThickness 1 +skinparam dpi 110 +skinparam svgLinkTarget _self + +[*] --> NEW + + +NEW -[#1E90FF,bold]-> PROCESSING <> <> : SUBMIT / processAction() +PROCESSING -[#1E90FF,bold]-> COMPLETED <> <> : FINISH + +COMPLETED --> [*] +@enduml + diff --git a/state_machines/multi_module_sample/api-module/build.gradle b/state_machines/multi_module_sample/api-module/build.gradle new file mode 100644 index 0000000..026c579 --- /dev/null +++ b/state_machines/multi_module_sample/api-module/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web:3.2.0' + implementation 'org.springframework.statemachine:spring-statemachine-core:3.2.0' +} diff --git a/state_machines/multi_module_sample/api-module/src/main/java/click/kamil/multi/api/OrderApi.java b/state_machines/multi_module_sample/api-module/src/main/java/click/kamil/multi/api/OrderApi.java new file mode 100644 index 0000000..41f7cf8 --- /dev/null +++ b/state_machines/multi_module_sample/api-module/src/main/java/click/kamil/multi/api/OrderApi.java @@ -0,0 +1,12 @@ +package click.kamil.multi.api; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +@RequestMapping("/orders") +public interface OrderApi { + + @PostMapping("/submit") + void submitOrder(@RequestBody String orderId); +} diff --git a/state_machines/multi_module_sample/api-module/src/main/java/click/kamil/multi/api/ProcessAction.java b/state_machines/multi_module_sample/api-module/src/main/java/click/kamil/multi/api/ProcessAction.java new file mode 100644 index 0000000..fa0ddb9 --- /dev/null +++ b/state_machines/multi_module_sample/api-module/src/main/java/click/kamil/multi/api/ProcessAction.java @@ -0,0 +1,12 @@ +package click.kamil.multi.api; + +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; + +public interface ProcessAction extends Action { + + @Override + default void execute(StateContext context) { + System.out.println("Processing order: " + context.getMessageHeader("orderId")); + } +} diff --git a/state_machines/multi_module_sample/core-module/build.gradle b/state_machines/multi_module_sample/core-module/build.gradle new file mode 100644 index 0000000..bb5d661 --- /dev/null +++ b/state_machines/multi_module_sample/core-module/build.gradle @@ -0,0 +1,13 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':state_machines:multi_module_sample:api-module') + implementation 'org.springframework.boot:spring-boot-starter-web:3.2.0' + implementation 'org.springframework.statemachine:spring-statemachine-core:3.2.0' +} diff --git a/state_machines/multi_module_sample/core-module/src/main/java/click/kamil/multi/core/OrderController.java b/state_machines/multi_module_sample/core-module/src/main/java/click/kamil/multi/core/OrderController.java new file mode 100644 index 0000000..5a55e0c --- /dev/null +++ b/state_machines/multi_module_sample/core-module/src/main/java/click/kamil/multi/core/OrderController.java @@ -0,0 +1,20 @@ +package click.kamil.multi.core; + +import click.kamil.multi.api.OrderApi; +import lombok.RequiredArgsConstructor; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.StateMachineFactory; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class OrderController implements OrderApi { + + private final StateMachineFactory factory; + + @Override + public void submitOrder(String orderId) { + StateMachine sm = factory.getStateMachine(orderId); + sm.sendEvent("SUBMIT"); + } +} diff --git a/state_machines/multi_module_sample/core-module/src/main/java/click/kamil/multi/core/OrderStateMachineConfig.java b/state_machines/multi_module_sample/core-module/src/main/java/click/kamil/multi/core/OrderStateMachineConfig.java new file mode 100644 index 0000000..7c12e9e --- /dev/null +++ b/state_machines/multi_module_sample/core-module/src/main/java/click/kamil/multi/core/OrderStateMachineConfig.java @@ -0,0 +1,43 @@ +package click.kamil.multi.core; + +import click.kamil.multi.api.ProcessAction; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.config.EnableStateMachineFactory; +import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +@Configuration +@EnableStateMachineFactory +public class OrderStateMachineConfig extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("NEW") + .state("PROCESSING") + .end("COMPLETED"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("NEW") + .target("PROCESSING") + .event("SUBMIT") + .action(processAction()) + .and() + .withExternal() + .source("PROCESSING") + .target("COMPLETED") + .event("FINISH"); + } + + @Bean + public ProcessAction processAction() { + return new ProcessAction() {}; + } +} diff --git a/state_machines/multi_module_sample/settings.gradle b/state_machines/multi_module_sample/settings.gradle new file mode 100644 index 0000000..a140b73 --- /dev/null +++ b/state_machines/multi_module_sample/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'multi-module-sample' +include 'api-module' +include 'core-module'