diff --git a/settings.gradle b/settings.gradle index 1d6ebc0..a64f302 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,6 +2,7 @@ rootProject.name = 'state machine exporter' include ':file_combine' include ':state_machine_exporter' +include ':state_machine_exporter_html' include ':state_machine_exporter_spring_based' include ':state_machines:complex_state_machine' include ':state_machines:dynamic_state_machine' diff --git a/state_machine_exporter/flow_renderer_plan/pocs/poc2_modern_light.html b/state_machine_exporter/flow_renderer_plan/pocs/poc2_modern_light.html new file mode 100644 index 0000000..9b738a5 --- /dev/null +++ b/state_machine_exporter/flow_renderer_plan/pocs/poc2_modern_light.html @@ -0,0 +1,417 @@ + + + + + + Final PoC: Real Machine Explorer + + + + + + + + + + +
+
+ + + + + + + + + + + + + + START + + + + + PROCESSING + + + + + COMPLETED + + + + + CANCELLED + + + + + + AUDIT_EVENT + + + + + SUBMIT_EVENT + + + + + REACTIVE_EVENT + + + + + EXTERNAL_TRIGGER + + + + + FINISH + + + + + CANCEL_EVENT + + +
+
+ + + + diff --git a/state_machine_exporter/out_html_verify/Verification.html b/state_machine_exporter/out_html_verify/Verification.html new file mode 100644 index 0000000..28ea8ed --- /dev/null +++ b/state_machine_exporter/out_html_verify/Verification.html @@ -0,0 +1,588 @@ + + + + + + State Machine Explorer - click.kamil.examples.statemachine.extended.config.ExtendedStateMachineConfig + + + + + + + + + + + + + + + + +
+
+ STARTPROCESSINGCOMPLETEDCANCELLEDINIT_STATESUBMIT_EVENTFINISHCANCEL_EVENTREACTIVE_EVENTAUDIT_EVENTEXTERNAL_TRIGGER +
+
+ + + + + + \ No newline at end of file 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 37dcbf6..2b415f2 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 @@ -472,4 +472,27 @@ public class CodebaseContext { public Collection getCompilationUnits() { return Collections.unmodifiableCollection(allCus); } + + public Collection getTypeDeclarations() { + List types = new ArrayList<>(); + for (CompilationUnit cu : allCus) { + for (Object typeObj : cu.types()) { + if (typeObj instanceof TypeDeclaration td) { + types.add(td); + // Handle nested types + types.addAll(getNestedTypes(td)); + } + } + } + return types; + } + + private List getNestedTypes(TypeDeclaration td) { + List nested = new ArrayList<>(); + for (TypeDeclaration inner : td.getTypes()) { + nested.add(inner); + nested.addAll(getNestedTypes(inner)); + } + return nested; + } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/ExportOptions.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/ExportOptions.java index 59c786b..809fd90 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/ExportOptions.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/ExportOptions.java @@ -10,4 +10,6 @@ public class ExportOptions { boolean useLambdaGuards = true; @Builder.Default boolean renderChoicesAsDiamonds = true; + @Builder.Default + boolean embedIdentifiers = false; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/PlantUml.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/PlantUml.java index 40f104c..c9613aa 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/PlantUml.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/PlantUml.java @@ -3,37 +3,17 @@ package click.kamil.springstatemachineexporter.exporter; import click.kamil.springstatemachineexporter.model.State; import click.kamil.springstatemachineexporter.model.Transition; import click.kamil.springstatemachineexporter.model.TransitionType; -import click.kamil.springstatemachineexporter.ast.common.StringUtils; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; public class PlantUml implements StateMachineExporter { - private final List choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4"); private final String LAMBDA = "λ"; - private String loadBaseStyle() { - try (InputStream is = getClass().getResourceAsStream("/plantuml/default-style.puml")) { - if (is == null) return ""; - return new BufferedReader(new InputStreamReader(is)) - .lines() - .collect(Collectors.joining("\n")); - } catch (Exception e) { - return ""; - } - } - @Override public String export(click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result, ExportOptions options) { return export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options); @@ -51,7 +31,26 @@ public class PlantUml implements StateMachineExporter { sb.append("!pragma layout smetana\n"); sb.append("hide empty description\n"); sb.append("hide stereotype\n"); - + + // --- Premium Modern Styles --- + sb.append("skinparam state {\n"); + sb.append(" BackgroundColor white\n"); + sb.append(" BorderColor #94a3b8\n"); + sb.append(" BorderThickness 1\n"); + sb.append(" FontName Inter\n"); + sb.append(" FontSize 9\n"); // Sleek but readable + sb.append(" FontStyle bold\n"); + sb.append(" RoundCorner 20\n"); + sb.append(" Padding 1\n"); + sb.append("}\n"); + sb.append("skinparam shadowing false\n"); + sb.append("skinparam ArrowFontName JetBrains Mono\n"); + sb.append("skinparam ArrowFontSize 8\n"); + sb.append("skinparam ArrowColor #cbd5e1\n"); + sb.append("skinparam ArrowThickness 1\n"); + sb.append("skinparam dpi 110\n"); + sb.append("skinparam svgLinkTarget _self\n"); + // Pre-declare all unique states for layout stability Set allUniqueStates = new LinkedHashSet<>(); transitions.forEach(t -> { @@ -61,21 +60,6 @@ public class PlantUml implements StateMachineExporter { allUniqueStates.forEach(s -> sb.append("state ").append(s).append("\n")); sb.append("\n"); - String style = loadBaseStyle(); - - StringBuilder choiceColorStyles = new StringBuilder(); - StringBuilder hideChoiceColors = new StringBuilder(); - for (int i = 0; i < choiceColors.size(); i++) { - choiceColorStyles.append(" .choice_color_").append(i) - .append(" { LineColor #").append(choiceColors.get(i)).append(" }\n"); - hideChoiceColors.append("hide <> stereotype\n"); - } - - style = style.replace("/* CHOICE_COLORS_PLACEHOLDER */", choiceColorStyles.toString()); - style = style.replace("/* HIDE_CHOICE_COLORS_PLACEHOLDER */", hideChoiceColors.toString()); - - sb.append(style).append("\n"); - for (String start : startStates) { sb.append("[*] --> ").append(simplify(start)).append("\n"); } @@ -90,16 +74,10 @@ public class PlantUml implements StateMachineExporter { } } - Map choiceStateClassMap = new HashMap<>(); - int colorIndex = 0; - for (String state : statesWithChoice) { - String className = "choice_color_" + (colorIndex % choiceColors.size()); if (options.isRenderChoicesAsDiamonds()) { sb.append("state ").append(state).append(" <>\n"); } - choiceStateClassMap.put(state, className); - colorIndex++; } Set junctionStates = transitions.stream() @@ -115,7 +93,6 @@ public class PlantUml implements StateMachineExporter { sb.append("\n"); - Set usedEventStereotypes = new java.util.LinkedHashSet<>(); for (Transition t : transitions) { if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue; @@ -139,29 +116,31 @@ public class PlantUml implements StateMachineExporter { for (String rawTarget : targets) { String target = simplify(rawTarget); - String styleClass = getStyleClass(t, source, choiceStateClassMap); - String color = getLegacyColor(t, source, choiceStateClassMap); - sb.append(source).append(" -[#").append(color).append(",bold]-> ").append(target).append(" <<").append(styleClass).append(">>"); + String styleClass = getStyleClass(t, source, Collections.emptyMap()); + String color = getLegacyColor(t, source, Collections.emptyMap()); + sb.append(source).append(" -[#").append(color).append(",bold]-> ").append(target); + + sb.append(" <<").append(styleClass).append(">>"); if (t.getEvent() != null && !t.getEvent().isBlank()) { - String eventClass = "e_" + normalize(t.getEvent()); - sb.append(" <<").append(eventClass).append(">>"); - usedEventStereotypes.add(eventClass); + sb.append(" <>"); } String label = buildLabel(t, options.isUseLambdaGuards()); if (!label.isEmpty()) { - sb.append(" : ").append(label); + sb.append(" : "); + if (options.isEmbedIdentifiers() && t.getEvent() != null && !t.getEvent().isBlank()) { + // Link the label text - this forces PlantUML to create an tag + sb.append("[[#link_").append(normalize(t.getEvent())).append(" ").append(label).append("]]"); + } else { + sb.append(label); + } } sb.append("\n"); } } } - for (String eventClass : usedEventStereotypes) { - sb.append("hide <<").append(eventClass).append(">> stereotype\n"); - } - sb.append("\n"); for (String end : endStates) { sb.append(simplify(end)).append(" --> [*]\n"); @@ -178,16 +157,9 @@ public class PlantUml implements StateMachineExporter { private String getLegacyColor(Transition t, String source, Map choiceStateClassMap) { if (t.getType() == null) - return "000000"; + return "94a3b8"; return switch (t.getType()) { - case CHOICE -> { - String className = choiceStateClassMap.get(source); - if (className != null && className.startsWith("choice_color_")) { - int index = Integer.parseInt(className.substring("choice_color_".length())); - yield choiceColors.get(index % choiceColors.size()); - } - yield "000000"; - } + case CHOICE -> "FF6347"; case EXTERNAL -> "1E90FF"; case INTERNAL -> "32CD32"; case LOCAL -> "FFA500"; @@ -201,7 +173,7 @@ public class PlantUml implements StateMachineExporter { if (t.getType() == null) return "default"; return switch (t.getType()) { - case CHOICE -> choiceStateClassMap.getOrDefault(source, "choice_type"); + case CHOICE -> "choice_type"; case EXTERNAL -> "external"; case INTERNAL -> "internal"; case LOCAL -> "local"; @@ -216,35 +188,30 @@ public class PlantUml implements StateMachineExporter { return ".puml"; } - private String getGuardText(Transition t, boolean useLambdaGuards) { - if (t.getGuard() == null || StringUtils.isBlank(t.getGuard().expression())) - return ""; - if (useLambdaGuards && t.getGuard().isLambda()) - return LAMBDA; - return t.getGuard().expression().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim(); - } - - private String getActionsText(Transition t, boolean useLambdaGuards) { - if (t.getActions() == null || t.getActions().isEmpty()) - return ""; - return t.getActions().stream() - .map(action -> (useLambdaGuards && action.isLambda()) ? LAMBDA : action.expression()) - .collect(Collectors.joining(", ")); + @Override + public String simplify(String state) { + if (state == null) return ""; + if (state.contains("\"")) return state; + return state.replaceAll("[^a-zA-Z0-9_]", ""); } private String buildLabel(Transition t, boolean useLambdaGuards) { List parts = new ArrayList<>(); - if (t.getEvent() != null && !t.getEvent().isBlank()) - parts.add(simplify(t.getEvent())); - String guard = getGuardText(t, useLambdaGuards); - if (!guard.isEmpty()) - parts.add("[" + guard + "]"); - String actions = getActionsText(t, useLambdaGuards); - if (!actions.isEmpty()) { - if (!parts.isEmpty()) - parts.add("/ " + actions); - else - parts.add(actions); + if (t.getEvent() != null && !t.getEvent().isBlank()) { + parts.add(t.getEvent()); + } + if (t.getGuard() != null) { + String g = t.getGuard().expression(); + if (useLambdaGuards && g.contains("->")) { + parts.add("[" + LAMBDA + "]"); + } else { + parts.add("[" + g + "]"); + } + } + if (t.getActions() != null && !t.getActions().isEmpty()) { + parts.add("/ " + t.getActions().stream() + .map(a -> a.expression().contains("->") ? LAMBDA : a.expression()) + .collect(Collectors.joining(", "))); } Optional.ofNullable(t.getOrder()) .filter(order -> t.getType() == TransitionType.CHOICE || t.getType() == TransitionType.JUNCTION) diff --git a/state_machine_exporter_html/build.gradle b/state_machine_exporter_html/build.gradle new file mode 100644 index 0000000..2e784c5 --- /dev/null +++ b/state_machine_exporter_html/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'java' + id 'application' +} + +group = 'click.kamil.springstatemachineexporter' +version = '1.0.0' + +application { + mainClass = 'click.kamil.springstatemachineexporter.html.Main' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':state_machine_exporter') + + implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' + implementation 'net.sourceforge.plantuml:plantuml:1.2024.3' + implementation 'org.eclipse.jdt:org.eclipse.jdt.core:3.38.0' + implementation 'org.slf4j:slf4j-api:2.0.12' + implementation 'info.picocli:picocli:4.7.6' + + compileOnly 'org.projectlombok:lombok:1.18.46' + annotationProcessor 'org.projectlombok:lombok:1.18.46' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.assertj:assertj-core:3.27.7' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/state_machine_exporter_html/out_enterprise/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig.html b/state_machine_exporter_html/out_enterprise/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig.html new file mode 100644 index 0000000..4f5b783 --- /dev/null +++ b/state_machine_exporter_html/out_enterprise/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig/click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig.html @@ -0,0 +1,643 @@ + + + + + + State Machine Explorer - click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig + + + + + + + + + + + + + + + + +
+
+ NEWCHECK_AVAILABILITYPENDING_PAYMENTCANCELLEDPAIDSHIPPEDDELIVEREDRETURNEDPLACE_ORDER[λ] (order=0)(order=1)PAY_ORDERSHIP_ORDERFINALIZECANCEL_ORDERRETURN_ORDER +
+
+ + + + + + diff --git a/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/Main.java b/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/Main.java new file mode 100644 index 0000000..a839515 --- /dev/null +++ b/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/Main.java @@ -0,0 +1,24 @@ +package click.kamil.springstatemachineexporter.html; + +import click.kamil.springstatemachineexporter.command.ExporterCommand; +import click.kamil.springstatemachineexporter.exporter.Dot; +import click.kamil.springstatemachineexporter.exporter.JsonExporter; +import click.kamil.springstatemachineexporter.exporter.PlantUml; +import click.kamil.springstatemachineexporter.exporter.Scxml; +import click.kamil.springstatemachineexporter.html.exporter.HtmlExporter; +import click.kamil.springstatemachineexporter.service.ExportService; +import picocli.CommandLine; + +import java.util.List; + +public class Main { + public static void main(String[] args) { + // Wiring including the new HTML exporter + var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter(), new HtmlExporter()); + var exportService = new ExportService(exporters); + var command = new ExporterCommand(exportService); + + int exitCode = new CommandLine(command).execute(args); + System.exit(exitCode); + } +} diff --git a/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/exporter/HtmlExporter.java b/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/exporter/HtmlExporter.java new file mode 100644 index 0000000..a3b763d --- /dev/null +++ b/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/exporter/HtmlExporter.java @@ -0,0 +1,97 @@ +package click.kamil.springstatemachineexporter.html.exporter; + +import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; +import click.kamil.springstatemachineexporter.exporter.ExportOptions; +import click.kamil.springstatemachineexporter.exporter.PlantUml; +import click.kamil.springstatemachineexporter.exporter.StateMachineExporter; +import click.kamil.springstatemachineexporter.model.Transition; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import net.sourceforge.plantuml.FileFormat; +import net.sourceforge.plantuml.FileFormatOption; +import net.sourceforge.plantuml.SourceStringReader; +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +public class HtmlExporter implements StateMachineExporter { + + private final PlantUml pumlExporter = new PlantUml(); + private final SvgDecorator svgDecorator = new SvgDecorator(); + private final ObjectMapper objectMapper = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); + + @Override + public String export(AnalysisResult result, ExportOptions options) { + try { + // 1. Generate PUML with identifiers embedded + ExportOptions enrichedOptions = ExportOptions.builder() + .useLambdaGuards(options.isUseLambdaGuards()) + .renderChoicesAsDiamonds(options.isRenderChoicesAsDiamonds()) + .embedIdentifiers(true) // Force identifying classes in SVG + .build(); + + String puml = pumlExporter.export(result, enrichedOptions); + + // 2. Generate SVG from PUML + SourceStringReader reader = new SourceStringReader(puml); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + reader.generateImage(os, new FileFormatOption(FileFormat.SVG)); + String rawSvg = os.toString(StandardCharsets.UTF_8); + + // 3. Decorate SVG (Clean responsive, styles, etc.) + String decoratedSvg = svgDecorator.decorate(rawSvg); + + // 4. Load Template + String template = loadTemplate(); + + // 5. Prepare Metadata JSON + String metadataJson = objectMapper.writeValueAsString(result); + + // 6. Inject Data + return template + .replace("{{MACHINE_NAME}}", result.getName()) + .replace("{{SVG_CONTENT}}", decoratedSvg) + .replace("{{METADATA_JSON}}", metadataJson); + + } catch (Exception e) { + log.error("Failed to export to HTML", e); + throw new RuntimeException("HTML export failed", e); + } + } + + private String loadTemplate() { + try (InputStream is = getClass().getResourceAsStream("/html/template.html")) { + if (is == null) throw new RuntimeException("HTML template not found"); + return new BufferedReader(new InputStreamReader(is)) + .lines() + .collect(Collectors.joining("\n")); + } catch (Exception e) { + throw new RuntimeException("Failed to load HTML template", e); + } + } + + @Override + public String export(String name, List transitions, Set startStates, Set endStates, ExportOptions options) { + AnalysisResult mockResult = AnalysisResult.builder() + .name(name) + .transitions(transitions) + .startStates(startStates) + .endStates(endStates) + .build(); + return export(mockResult, options); + } + + @Override + public String getFileExtension() { + return ".html"; + } +} diff --git a/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/exporter/SvgDecorator.java b/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/exporter/SvgDecorator.java new file mode 100644 index 0000000..97698f5 --- /dev/null +++ b/state_machine_exporter_html/src/main/java/click/kamil/springstatemachineexporter/html/exporter/SvgDecorator.java @@ -0,0 +1,68 @@ +package click.kamil.springstatemachineexporter.html.exporter; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SvgDecorator { + + public String decorate(String rawSvg) { + String result = rawSvg; + + // 1. Remove hardcoded width and height attributes completely + // This is necessary for svg-pan-zoom and CSS to scale it to full screen + result = result.replaceFirst("(?i)width=\"[^\"]*\"", ""); + result = result.replaceFirst("(?i)height=\"[^\"]*\"", ""); + + // 2. Remove the hardcoded 'style' attribute which often contains fixed pixel sizes + result = result.replaceFirst("(?i)style=\"[^\"]*\"", ""); + + // 3. Maintain layout integrity + result = result.replace("preserveAspectRatio=\"none\"", "preserveAspectRatio=\"xMidYMid meet\""); + + // 4. Inject CSS for interactivity (Highlights, Halos, Animations) + String styleBlock = """ + + """; + + int defsEnd = result.indexOf(""); + if (defsEnd != -1) { + result = result.substring(0, defsEnd + 7) + styleBlock + result.substring(defsEnd + 7); + } + + return result; + } +} diff --git a/state_machine_exporter_html/src/main/resources/html/template.html b/state_machine_exporter_html/src/main/resources/html/template.html new file mode 100644 index 0000000..be314b6 --- /dev/null +++ b/state_machine_exporter_html/src/main/resources/html/template.html @@ -0,0 +1,360 @@ + + + + + + State Machine Explorer - {{MACHINE_NAME}} + + + + + + + + + + + + + + + + +
+
+ {{SVG_CONTENT}} +
+
+ + + + + + diff --git a/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlExporterTest.java b/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlExporterTest.java new file mode 100644 index 0000000..9a09998 --- /dev/null +++ b/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlExporterTest.java @@ -0,0 +1,82 @@ +package click.kamil.springstatemachineexporter.html; + +import click.kamil.springstatemachineexporter.analysis.enricher.CallChainEnricher; +import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnricher; +import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher; +import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher; +import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; +import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService; +import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import click.kamil.springstatemachineexporter.exporter.ExportOptions; +import click.kamil.springstatemachineexporter.html.exporter.HtmlExporter; +import click.kamil.springstatemachineexporter.model.Transition; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.junit.jupiter.api.Test; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HtmlExporterTest { + + @Test + void verifyFirstStepOfGeneration() throws IOException { + // 1. Setup paths + Path projectRoot = Path.of("..").toAbsolutePath().normalize(); + Path inputDir = projectRoot.resolve("state_machines/extended_analysis_sample"); + Path outputDir = projectRoot.resolve("state_machine_exporter/out_html_verify"); + + Files.createDirectories(outputDir); + + // 2. Perform Analysis (Same as core Main) + CodebaseContext context = new CodebaseContext(); + context.setSourcepath(List.of(inputDir.toString())); + context.setResolveBindings(true); + context.scan(inputDir); + + // Find the ExtendedStateMachineConfig class + TypeDeclaration td = context.getTypeDeclarations().stream() + .filter(t -> context.getFqn(t).equals("click.kamil.examples.statemachine.extended.config.ExtendedStateMachineConfig")) + .findFirst() + .orElseThrow(); + + StateMachineAggregator aggregator = new StateMachineAggregator(context); + List transitions = aggregator.aggregateTransitions(td); + aggregator.aggregateStates(td); + + AnalysisResult result = AnalysisResult.builder() + .name(context.getFqn(td)) + .transitions(transitions) + .startStates(aggregator.getInitialStates()) + .endStates(aggregator.getEndStates()) + .build(); + + // 3. Perform Enrichment + EnrichmentService enrichmentService = new EnrichmentService(List.of( + new TriggerEnricher(), + new EntryPointEnricher(), + new PropertyEnricher(), + new CallChainEnricher() + )); + enrichmentService.enrich(result, context, new click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider(context, inputDir)); + + // 4. Run HtmlExporter + HtmlExporter exporter = new HtmlExporter(); + String html = exporter.export(result, ExportOptions.builder().build()); + + Path outputFile = outputDir.resolve("Verification.html"); + Files.writeString(outputFile, html); + System.out.println("Verification HTML generated at: " + outputFile.toAbsolutePath()); + + // 5. Verification + assertThat(html).contains(""); + assertThat(html).contains("Explorer"); + assertThat(html).contains(result.getName()); + assertThat(html).contains("