cli
This commit is contained in:
@@ -1,124 +1,22 @@
|
||||
package click.kamil.springstatemachineexporter;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser;
|
||||
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
|
||||
import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils;
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.ast.out.Dot;
|
||||
import click.kamil.springstatemachineexporter.ast.out.PlantUml;
|
||||
import click.kamil.springstatemachineexporter.ast.out.Scxml;
|
||||
import click.kamil.springstatemachineexporter.ast.out.StateMachineExporter;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import click.kamil.springstatemachineexporter.command.ExporterCommand;
|
||||
import click.kamil.springstatemachineexporter.service.ExportService;
|
||||
import picocli.CommandLine;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class Main {
|
||||
private static final String START_DIR = ".";
|
||||
private static final String OUTPUT_DIR = "./out";
|
||||
private static final List<String> TARGET_ANNOTATIONS = List.of("EnableStateMachineFactory", "EnableStateMachine");
|
||||
public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
|
||||
public static void main(String[] args) {
|
||||
// Manual DI / Wiring
|
||||
var exporters = List.of(new PlantUml(), new Dot(), new Scxml());
|
||||
var exportService = new ExportService(exporters);
|
||||
var command = new ExporterCommand(exportService);
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
runExporter(Path.of(START_DIR), Path.of(OUTPUT_DIR));
|
||||
}
|
||||
|
||||
public static void runExporter(Path inputDir, Path outputDir) throws IOException {
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(inputDir);
|
||||
|
||||
List<StateMachineExporter> outputs = List.of(
|
||||
new PlantUml(),
|
||||
new Dot(),
|
||||
new Scxml()
|
||||
);
|
||||
|
||||
exportAll(context, outputs, outputDir);
|
||||
}
|
||||
|
||||
public static void exportAll(CodebaseContext context, List<StateMachineExporter> outputs, Path outputDir) throws IOException {
|
||||
Set<String> processedLocations = new HashSet<>();
|
||||
|
||||
// 1. Find entry point classes (annotated or extending adapter)
|
||||
List<TypeDeclaration> entryPoints = context.findEntryPointClasses(TARGET_ANNOTATIONS);
|
||||
for (TypeDeclaration td : entryPoints) {
|
||||
String fqn = context.getFqn(td);
|
||||
if (processedLocations.add(fqn)) {
|
||||
processEntryPoint(td, outputs, outputDir, context);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Find methods returning StateMachine beans
|
||||
List<MethodDeclaration> beanMethods = context.findBeanMethodsReturning(STATE_MACHINE_RETURN_TYPES);
|
||||
for (MethodDeclaration m : beanMethods) {
|
||||
TypeDeclaration parentClass = (TypeDeclaration) m.getParent();
|
||||
String parentFqn = context.getFqn(parentClass);
|
||||
String methodName = m.getName().getIdentifier();
|
||||
String uniqueId = parentFqn + "#" + methodName;
|
||||
|
||||
if (processedLocations.add(uniqueId)) {
|
||||
processBeanMethod(m, outputs, outputDir, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void processEntryPoint(TypeDeclaration td, List<StateMachineExporter> outputs, Path outputDir, CodebaseContext context) throws IOException {
|
||||
String className = context.getFqn(td);
|
||||
System.out.println("Processing state machine config: " + className);
|
||||
|
||||
StateMachineAggregator aggregator = new StateMachineAggregator(context);
|
||||
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
||||
|
||||
aggregator.aggregateStates(td);
|
||||
Set<String> initialStatesAst = aggregator.getInitialStates();
|
||||
Set<String> endStatesAst = aggregator.getEndStates();
|
||||
|
||||
System.out.println("Start States Ast: " + initialStatesAst);
|
||||
System.out.println("End States Ast: " + endStatesAst);
|
||||
|
||||
Set<String> startStates = TransitionStateUtils.findStartStates(transitions, initialStatesAst);
|
||||
Set<String> endStates = TransitionStateUtils.findEndStates(transitions, endStatesAst);
|
||||
|
||||
generateOutputs(outputDir, className, transitions, startStates, endStates, outputs);
|
||||
}
|
||||
|
||||
static void processBeanMethod(MethodDeclaration m, List<StateMachineExporter> outputs, Path outputDir, CodebaseContext context) throws IOException {
|
||||
TypeDeclaration parentClass = (TypeDeclaration) m.getParent();
|
||||
String parentFqn = context.getFqn(parentClass);
|
||||
String methodName = m.getName().getIdentifier();
|
||||
String uniqueName = parentFqn + "#" + methodName;
|
||||
|
||||
System.out.println("Processing state machine bean: " + uniqueName);
|
||||
|
||||
List<Transition> transitions = AstTransitionParser.parseTransitions(m, context);
|
||||
|
||||
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
|
||||
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
|
||||
|
||||
generateOutputs(outputDir, uniqueName, transitions, startStates, endStates, outputs);
|
||||
}
|
||||
|
||||
private static void generateOutputs(Path outputDir, String baseName, List<Transition> transitions,
|
||||
Set<String> startStates, Set<String> endStates,
|
||||
List<StateMachineExporter> outputs) throws IOException {
|
||||
|
||||
Path targetDir = outputDir.resolve(baseName);
|
||||
Files.createDirectories(targetDir);
|
||||
|
||||
for (StateMachineExporter output : outputs) {
|
||||
String content = output.export(transitions, startStates, endStates, true);
|
||||
String fileName = baseName + output.getFileExtension();
|
||||
|
||||
try (PrintWriter out = new PrintWriter(targetDir.resolve(fileName).toFile())) {
|
||||
out.println(content);
|
||||
}
|
||||
}
|
||||
int exitCode = new CommandLine(command).execute(args);
|
||||
System.exit(exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.util.Set;
|
||||
|
||||
public class Dot implements StateMachineExporter {
|
||||
|
||||
|
||||
private static final String[] CHOICE_COLORS = {
|
||||
"blue", "green", "red", "purple", "orange", "brown", "darkgreen", "darkblue"
|
||||
};
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package click.kamil.springstatemachineexporter.command;
|
||||
|
||||
import click.kamil.springstatemachineexporter.service.ExportService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
@Command(
|
||||
name = "spring-sm-exporter",
|
||||
mixinStandardHelpOptions = true,
|
||||
version = "1.0.0",
|
||||
description = "Exports Spring State Machine configurations to UML, DOT, and SCXML using AST analysis.",
|
||||
headerHeading = "@|bold,underline Usage:|@%n%n",
|
||||
synopsisHeading = "%n",
|
||||
descriptionHeading = "%n@|bold,underline Description:|@%n%n",
|
||||
parameterListHeading = "%n@|bold,underline Parameters:|@%n",
|
||||
optionListHeading = "%n@|bold,underline Options:|@%n",
|
||||
header = "Welcome to the @|bold,blue Spring State Machine Renderer|@!%n"
|
||||
)
|
||||
@RequiredArgsConstructor
|
||||
public class ExporterCommand implements Callable<Integer> {
|
||||
|
||||
private final ExportService exportService;
|
||||
|
||||
@CommandLine.Spec
|
||||
CommandLine.Model.CommandSpec spec;
|
||||
|
||||
@Option(names = {"-i", "--input"}, description = "Input directory containing the Spring Boot project source code.", defaultValue = ".")
|
||||
private Path inputDir;
|
||||
|
||||
@Option(names = {"-o", "--output"}, description = "Output directory for the generated diagrams.", defaultValue = "./out")
|
||||
private Path outputDir;
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
var out = spec.commandLine().getOut();
|
||||
var err = spec.commandLine().getErr();
|
||||
|
||||
out.println(CommandLine.Help.Ansi.AUTO.string("@|bold,green Starting export process...|@"));
|
||||
out.println(CommandLine.Help.Ansi.AUTO.string("@|yellow Input Directory:|@ " + inputDir.toAbsolutePath()));
|
||||
out.println(CommandLine.Help.Ansi.AUTO.string("@|yellow Output Directory:|@ " + outputDir.toAbsolutePath()));
|
||||
|
||||
try {
|
||||
exportService.runExporter(inputDir, outputDir);
|
||||
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath()));
|
||||
return 0;
|
||||
} catch (Exception e) {
|
||||
err.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,red ERROR:|@ An error occurred during the export process: " + e.getMessage()));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package click.kamil.springstatemachineexporter.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser;
|
||||
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
|
||||
import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils;
|
||||
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.ast.out.StateMachineExporter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ExportService {
|
||||
|
||||
private final List<StateMachineExporter> exporters;
|
||||
private static final List<String> TARGET_ANNOTATIONS = List.of("EnableStateMachineFactory", "EnableStateMachine");
|
||||
public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
|
||||
|
||||
public void runExporter(Path inputDir, Path outputDir) throws IOException {
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(inputDir);
|
||||
exportAll(context, outputDir);
|
||||
}
|
||||
|
||||
private void exportAll(CodebaseContext context, Path outputDir) throws IOException {
|
||||
Set<String> processedLocations = new HashSet<>();
|
||||
|
||||
// 1. Find entry point classes (annotated or extending adapter)
|
||||
List<TypeDeclaration> entryPoints = context.findEntryPointClasses(TARGET_ANNOTATIONS);
|
||||
for (TypeDeclaration td : entryPoints) {
|
||||
String fqn = context.getFqn(td);
|
||||
if (processedLocations.add(fqn)) {
|
||||
processEntryPoint(td, outputDir, context);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Find methods returning StateMachine beans
|
||||
List<MethodDeclaration> beanMethods = context.findBeanMethodsReturning(STATE_MACHINE_RETURN_TYPES);
|
||||
for (MethodDeclaration m : beanMethods) {
|
||||
TypeDeclaration parentClass = (TypeDeclaration) m.getParent();
|
||||
String parentFqn = context.getFqn(parentClass);
|
||||
String methodName = m.getName().getIdentifier();
|
||||
String uniqueId = parentFqn + "#" + methodName;
|
||||
|
||||
if (processedLocations.add(uniqueId)) {
|
||||
processBeanMethod(m, outputDir, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context) throws IOException {
|
||||
String className = context.getFqn(td);
|
||||
log.info("Processing state machine config: {}", className);
|
||||
|
||||
StateMachineAggregator aggregator = new StateMachineAggregator(context);
|
||||
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
||||
|
||||
aggregator.aggregateStates(td);
|
||||
Set<String> initialStatesAst = aggregator.getInitialStates();
|
||||
Set<String> endStatesAst = aggregator.getEndStates();
|
||||
|
||||
log.debug("Start States Ast: {}", initialStatesAst);
|
||||
log.debug("End States Ast: {}", endStatesAst);
|
||||
|
||||
Set<String> startStates = TransitionStateUtils.findStartStates(transitions, initialStatesAst);
|
||||
Set<String> endStates = TransitionStateUtils.findEndStates(transitions, endStatesAst);
|
||||
|
||||
generateOutputs(outputDir, className, transitions, startStates, endStates);
|
||||
}
|
||||
|
||||
private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context) throws IOException {
|
||||
TypeDeclaration parentClass = (TypeDeclaration) m.getParent();
|
||||
String parentFqn = context.getFqn(parentClass);
|
||||
String methodName = m.getName().getIdentifier();
|
||||
String uniqueName = parentFqn + "#" + methodName;
|
||||
|
||||
log.info("Processing state machine bean: {}", uniqueName);
|
||||
|
||||
List<Transition> transitions = AstTransitionParser.parseTransitions(m, context);
|
||||
|
||||
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
|
||||
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
|
||||
|
||||
generateOutputs(outputDir, uniqueName, transitions, startStates, endStates);
|
||||
}
|
||||
|
||||
private void generateOutputs(Path outputDir, String baseName, List<Transition> transitions,
|
||||
Set<String> startStates, Set<String> endStates) throws IOException {
|
||||
|
||||
Path targetDir = outputDir.resolve(baseName);
|
||||
Files.createDirectories(targetDir);
|
||||
|
||||
for (StateMachineExporter output : exporters) {
|
||||
String content = output.export(transitions, startStates, endStates, true);
|
||||
String fileName = baseName + output.getFileExtension();
|
||||
|
||||
try (PrintWriter out = new PrintWriter(targetDir.resolve(fileName).toFile())) {
|
||||
out.println(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user