From 7c2c84df2f47a338bdcb196ac9543b49cf119bee Mon Sep 17 00:00:00 2001 From: junietrytwo Date: Tue, 7 Apr 2026 17:56:22 +0800 Subject: [PATCH] https://github.com/spring-projects/spring-modulith/issues/1048 --- pom.xml | 1 + spring-modulith-maven-plugin/pom.xml | 120 +++++++++ .../modulith/maven/DiagramRenderer.java | 22 ++ .../modulith/maven/ModulithReportMojo.java | 162 ++++++++++++ .../maven/ModulithSiteReportGenerator.java | 242 ++++++++++++++++++ .../modulith/maven/PlantUmlSvgRenderer.java | 62 +++++ .../maven/ProjectClassLoaderFactory.java | 69 +++++ .../modulith/maven/RenderedDiagram.java | 26 ++ .../ModulithSiteReportGeneratorUnitTests.java | 78 ++++++ .../maven/sample/SampleApplication.java | 8 + .../sample/inventory/InventoryManagement.java | 12 + .../maven/sample/order/OrderManagement.java | 19 ++ 12 files changed, 821 insertions(+) create mode 100644 spring-modulith-maven-plugin/pom.xml create mode 100644 spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/DiagramRenderer.java create mode 100644 spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/ModulithReportMojo.java create mode 100644 spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/ModulithSiteReportGenerator.java create mode 100644 spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/PlantUmlSvgRenderer.java create mode 100644 spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/ProjectClassLoaderFactory.java create mode 100644 spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/RenderedDiagram.java create mode 100644 spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/ModulithSiteReportGeneratorUnitTests.java create mode 100644 spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/sample/SampleApplication.java create mode 100644 spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/sample/inventory/InventoryManagement.java create mode 100644 spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/sample/order/OrderManagement.java diff --git a/pom.xml b/pom.xml index 9cf0b18a..6357785a 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,7 @@ spring-modulith-bom spring-modulith-core spring-modulith-docs + spring-modulith-maven-plugin spring-modulith-events spring-modulith-junit spring-modulith-moments diff --git a/spring-modulith-maven-plugin/pom.xml b/spring-modulith-maven-plugin/pom.xml new file mode 100644 index 00000000..9350c130 --- /dev/null +++ b/spring-modulith-maven-plugin/pom.xml @@ -0,0 +1,120 @@ + + 4.0.0 + + + org.springframework.modulith + spring-modulith + 2.1.0-SNAPSHOT + ../pom.xml + + + spring-modulith-maven-plugin + maven-plugin + Spring Modulith - Maven Plugin + Maven plugin and site report support for Spring Modulith documentation. + + + spring.modulith.maven.plugin + 3.8.6 + 3.1.0 + 3.15.2 + 1.2024.8 + + + + + + ${project.groupId} + spring-modulith-docs + ${project.version} + + + + net.sourceforge.plantuml + plantuml + ${plantuml.version} + + + + org.yaml + snakeyaml + + + + org.apache.maven + maven-plugin-api + ${maven.api.version} + provided + + + + org.apache.maven + maven-core + ${maven.api.version} + provided + + + + org.apache.maven + maven-artifact + ${maven.api.version} + provided + + + + org.apache.maven.plugin-tools + maven-plugin-annotations + ${maven.plugin.tools.version} + provided + + + + org.apache.maven.reporting + maven-reporting-api + ${maven.reporting.version} + + + + org.apache.maven.reporting + maven-reporting-impl + ${maven.reporting.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + ${maven.plugin.tools.version} + + modulith + + + + default-descriptor + + descriptor + + + + help-goal + + helpmojo + + + + + + + + + + diff --git a/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/DiagramRenderer.java b/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/DiagramRenderer.java new file mode 100644 index 00000000..a44f2e8c --- /dev/null +++ b/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/DiagramRenderer.java @@ -0,0 +1,22 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.maven; + +interface DiagramRenderer { + + RenderedDiagram render(String source); +} + diff --git a/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/ModulithReportMojo.java b/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/ModulithReportMojo.java new file mode 100644 index 00000000..99eed324 --- /dev/null +++ b/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/ModulithReportMojo.java @@ -0,0 +1,162 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.maven; + +import java.io.File; +import java.net.URLClassLoader; +import java.util.Locale; + +import org.apache.maven.doxia.siterenderer.Renderer; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Execute; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; +import org.apache.maven.reporting.AbstractMavenReport; +import org.apache.maven.reporting.MavenReportException; +import org.springframework.modulith.core.ApplicationModules; +import org.springframework.modulith.docs.Documenter.DiagramOptions.DiagramStyle; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +@Mojo(name = "report", defaultPhase = LifecyclePhase.SITE, threadSafe = true, + requiresDependencyResolution = ResolutionScope.RUNTIME) +@Execute(phase = LifecyclePhase.COMPILE) +public class ModulithReportMojo extends AbstractMavenReport { + + @Component + private Renderer siteRenderer; + + @Parameter(defaultValue = "${project}", required = true, readonly = true) + private MavenProject project; + + @Parameter(property = "spring.modulith.report.skip", defaultValue = "false") + private boolean skip; + + @Parameter(property = "spring.modulith.report.applicationClassName") + private String applicationClassName; + + @Parameter(property = "spring.modulith.report.basePackage") + private String basePackage; + + @Parameter(property = "spring.modulith.report.name", defaultValue = "Spring Modulith Report") + private String siteReportName; + + @Parameter(property = "spring.modulith.report.description", defaultValue = "Application module dependency diagrams generated from Spring Modulith metadata") + private String siteReportDescription; + + @Parameter(property = "spring.modulith.report.outputdir", defaultValue = "modulith") + private String siteReportDirectory; + + @Parameter(property = "spring.modulith.report.renderDiagrams", defaultValue = "true") + private boolean renderDiagrams; + + @Parameter(property = "spring.modulith.report.diagramStyle", defaultValue = "C4") + private DiagramStyle diagramStyle; + + private final ProjectClassLoaderFactory classLoaderFactory = new ProjectClassLoaderFactory(); + private final ModulithSiteReportGenerator generator = new ModulithSiteReportGenerator(new PlantUmlSvgRenderer()); + + @Override + public String getOutputName() { + return siteReportDirectory + File.separator + "index"; + } + + @Override + public String getName(Locale locale) { + return siteReportName; + } + + @Override + public String getDescription(Locale locale) { + return siteReportDescription; + } + + @Override + protected Renderer getSiteRenderer() { + return siteRenderer; + } + + @Override + protected String getOutputDirectory() { + return getReportOutputDirectory().getAbsolutePath(); + } + + @Override + protected MavenProject getProject() { + return project; + } + + @Override + protected void executeReport(Locale locale) throws MavenReportException { + + validateConfiguration(); + + try (URLClassLoader classLoader = classLoaderFactory.create(project)) { + + var original = Thread.currentThread().getContextClassLoader(); + + try { + Thread.currentThread().setContextClassLoader(classLoader); + + var modules = createApplicationModules(classLoader); + var outputDirectory = getReportOutputDirectory().toPath().resolve(siteReportDirectory); + generator.generate(modules, outputDirectory, diagramStyle, renderDiagrams); + + } finally { + Thread.currentThread().setContextClassLoader(original); + } + + } catch (Exception ex) { + throw new MavenReportException("Failed to generate Spring Modulith site report", ex); + } + } + + @Override + public boolean canGenerateReport() { + return !skip; + } + + @Override + public boolean isExternalReport() { + return true; + } + + private ApplicationModules createApplicationModules(ClassLoader classLoader) throws ClassNotFoundException { + + if (applicationClassName != null && !applicationClassName.isBlank()) { + Class applicationType = ClassUtils.forName(applicationClassName, classLoader); + return ApplicationModules.of(applicationType); + } + + Assert.hasText(basePackage, "Either applicationClassName or basePackage must be configured!"); + return ApplicationModules.of(basePackage); + } + + private void validateConfiguration() { + + boolean hasApplicationClass = applicationClassName != null && !applicationClassName.isBlank(); + boolean hasBasePackage = basePackage != null && !basePackage.isBlank(); + + if (hasApplicationClass == hasBasePackage) { + throw new IllegalArgumentException( + "Configure exactly one of spring.modulith.report.applicationClassName or spring.modulith.report.basePackage."); + } + } +} + diff --git a/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/ModulithSiteReportGenerator.java b/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/ModulithSiteReportGenerator.java new file mode 100644 index 00000000..419e94f8 --- /dev/null +++ b/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/ModulithSiteReportGenerator.java @@ -0,0 +1,242 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.maven; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.modulith.core.ApplicationModule; +import org.springframework.modulith.core.ApplicationModules; +import org.springframework.modulith.docs.Documenter; +import org.springframework.modulith.docs.Documenter.DiagramOptions; +import org.springframework.modulith.docs.Documenter.DiagramOptions.DiagramStyle; +import org.springframework.util.Assert; + +final class ModulithSiteReportGenerator { + + private static final String OVERVIEW_FILE = "components.puml"; + private static final String MODULE_FILE_PATTERN = "module-%s.puml"; + + private final DiagramRenderer renderer; + + ModulithSiteReportGenerator(DiagramRenderer renderer) { + this.renderer = renderer; + } + + void generate(ApplicationModules modules, Path reportDirectory, DiagramStyle diagramStyle, boolean renderDiagrams) { + + Assert.notNull(modules, "ApplicationModules must not be null!"); + Assert.notNull(reportDirectory, "Report directory must not be null!"); + Assert.notNull(diagramStyle, "Diagram style must not be null!"); + + try { + + Files.createDirectories(reportDirectory); + + var assets = reportDirectory.resolve("assets"); + Files.createDirectories(assets); + + var diagrams = generateDiagramArtifacts(modules, assets, diagramStyle, renderDiagrams); + Files.writeString(reportDirectory.resolve("index.html"), renderHtml(modules, diagramStyle, diagrams), + StandardCharsets.UTF_8); + + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private List generateDiagramArtifacts(ApplicationModules modules, Path assets, + DiagramStyle diagramStyle, boolean renderDiagrams) throws IOException { + + var tempDirectory = Files.createTempDirectory("spring-modulith-site-report-"); + + try { + + var options = Documenter.Options.defaults().withOutputFolder(tempDirectory.toString()).withoutClean(); + var documenter = new Documenter(modules, options); + var diagramOptions = DiagramOptions.defaults().withStyle(diagramStyle); + + documenter.writeModulesAsPlantUml(diagramOptions); + documenter.writeIndividualModulesAsPlantUml(diagramOptions); + + var result = new ArrayList(); + result.add(copyDiagram("Overview", "Application module overview", OVERVIEW_FILE, tempDirectory, assets, + renderDiagrams)); + + for (ApplicationModule module : modules) { + var moduleId = module.getIdentifier().toString(); + var fileName = MODULE_FILE_PATTERN.formatted(moduleId); + result.add(copyDiagram(module.getDisplayName(), "Dependencies of module '" + moduleId + "'", fileName, + tempDirectory, assets, renderDiagrams)); + } + + return result; + + } finally { + deleteRecursively(tempDirectory); + } + } + + private GeneratedDiagram copyDiagram(String title, String description, String fileName, Path sourceDirectory, + Path assets, boolean renderDiagrams) throws IOException { + + var source = Files.readString(sourceDirectory.resolve(fileName), StandardCharsets.UTF_8); + var pumlTarget = assets.resolve(fileName); + Files.writeString(pumlTarget, source, StandardCharsets.UTF_8); + + String svgFile = null; + String failureMessage = null; + + if (renderDiagrams) { + + var rendered = renderer.render(source); + failureMessage = rendered.failureMessage(); + + if (rendered.hasSvg()) { + svgFile = fileName.replace(".puml", ".svg"); + Files.writeString(assets.resolve(svgFile), rendered.svg(), StandardCharsets.UTF_8); + } + } + + return new GeneratedDiagram(title, description, "assets/" + fileName, + svgFile == null ? null : "assets/" + svgFile, source, failureMessage); + } + + private String renderHtml(ApplicationModules modules, DiagramStyle diagramStyle, List diagrams) { + + var html = new StringBuilder(); + + html.append("\n") + .append("\n") + .append("\n") + .append(" \n") + .append(" \n") + .append(" Spring Modulith Report\n") + .append(" \n") + .append("\n") + .append("\n") + .append("

Spring Modulith Report

\n") + .append("

Generated ") + .append(escapeHtml(Instant.now().toString())) + .append(" from ") + .append(escapeHtml(modules.getSource().toString())) + .append(" using diagram style ") + .append(escapeHtml(diagramStyle.name())) + .append(".

\n"); + + for (GeneratedDiagram diagram : diagrams) { + html.append("
\n") + .append("

") + .append(escapeHtml(diagram.title())) + .append("

\n") + .append("

") + .append(escapeHtml(diagram.description())) + .append("

\n") + .append("

PlantUML source"); + + if (diagram.svgLocation() != null) { + html.append("SVG"); + } + + html.append("

\n"); + + if (diagram.svgLocation() != null) { + html.append("

\"")

\n"); + } else { + html.append("
No SVG preview is available for this diagram."); + + if (diagram.failureMessage() != null && !diagram.failureMessage().isBlank()) { + html.append(" PlantUML rendering reported: ") + .append(escapeHtml(diagram.failureMessage())) + .append('.'); + } + + html.append(" The exact PlantUML source is included below.
\n"); + } + + html.append("
Show PlantUML source
")
+					.append(escapeHtml(diagram.source()))
+					.append("
\n") + .append("
\n"); + } + + html.append("\n\n"); + + return html.toString(); + } + + private static String escapeHtml(String value) { + return value.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + private static void deleteRecursively(Path directory) { + + if (directory == null) { + return; + } + + try { + if (!Files.exists(directory)) { + return; + } + + Files.walk(directory) + .sorted((left, right) -> right.compareTo(left)) + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private record GeneratedDiagram(String title, String description, String pumlLocation, String svgLocation, + String source, String failureMessage) {} +} + diff --git a/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/PlantUmlSvgRenderer.java b/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/PlantUmlSvgRenderer.java new file mode 100644 index 00000000..caa70b3e --- /dev/null +++ b/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/PlantUmlSvgRenderer.java @@ -0,0 +1,62 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.maven; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; + +import net.sourceforge.plantuml.FileFormat; +import net.sourceforge.plantuml.FileFormatOption; +import net.sourceforge.plantuml.SourceStringReader; + +final class PlantUmlSvgRenderer implements DiagramRenderer { + + private static final String START = "@startuml"; + private static final String LAYOUT = "!pragma layout smetana"; + + @Override + public RenderedDiagram render(String source) { + + try { + + var output = new ByteArrayOutputStream(); + var reader = new SourceStringReader(prepare(source)); + var description = reader.outputImage(output, new FileFormatOption(FileFormat.SVG)); + var svg = output.toString(StandardCharsets.UTF_8); + + if (svg.isBlank()) { + return new RenderedDiagram(null, description == null ? "PlantUML did not produce SVG output." + : description.getDescription()); + } + + return new RenderedDiagram(svg, null); + + } catch (Exception ex) { + return new RenderedDiagram(null, ex.getMessage()); + } + } + + private static String prepare(String source) { + + if (!source.contains(START) || source.contains(LAYOUT)) { + return source; + } + + return source.replaceFirst(START, START + System.lineSeparator() + System.lineSeparator() + LAYOUT); + } +} + + diff --git a/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/ProjectClassLoaderFactory.java b/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/ProjectClassLoaderFactory.java new file mode 100644 index 00000000..5b2f1f13 --- /dev/null +++ b/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/ProjectClassLoaderFactory.java @@ -0,0 +1,69 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.maven; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; + +import org.apache.maven.artifact.DependencyResolutionRequiredException; +import org.apache.maven.project.MavenProject; + +final class ProjectClassLoaderFactory { + + URLClassLoader create(MavenProject project) throws DependencyResolutionRequiredException { + + var elements = new LinkedHashSet(); + var build = project.getBuild(); + + if (build != null) { + addIfHasText(elements, build.getOutputDirectory()); + } + + elements.addAll(project.getRuntimeClasspathElements()); + + List urls = new ArrayList<>(); + + for (String element : elements) { + if (element == null || element.isBlank()) { + continue; + } + + File file = Path.of(element).toFile(); + + if (file.exists()) { + try { + urls.add(file.toURI().toURL()); + } catch (Exception ex) { + throw new IllegalStateException("Failed to turn classpath element into URL: " + element, ex); + } + } + } + + return new URLClassLoader(urls.toArray(URL[]::new), getClass().getClassLoader()); + } + + private static void addIfHasText(LinkedHashSet elements, String candidate) { + if (candidate != null && !candidate.isBlank()) { + elements.add(candidate); + } + } +} + diff --git a/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/RenderedDiagram.java b/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/RenderedDiagram.java new file mode 100644 index 00000000..e6cab301 --- /dev/null +++ b/spring-modulith-maven-plugin/src/main/java/org/springframework/modulith/maven/RenderedDiagram.java @@ -0,0 +1,26 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.maven; + +import org.jspecify.annotations.Nullable; + +record RenderedDiagram(@Nullable String svg, @Nullable String failureMessage) { + + boolean hasSvg() { + return svg != null && !svg.isBlank(); + } +} + diff --git a/spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/ModulithSiteReportGeneratorUnitTests.java b/spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/ModulithSiteReportGeneratorUnitTests.java new file mode 100644 index 00000000..5c2ff6e3 --- /dev/null +++ b/spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/ModulithSiteReportGeneratorUnitTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.maven; + +import static org.assertj.core.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.modulith.core.ApplicationModules; +import org.springframework.modulith.maven.sample.SampleApplication; +import org.springframework.modulith.docs.Documenter.DiagramOptions.DiagramStyle; + +import com.tngtech.archunit.core.importer.ImportOption; + +class ModulithSiteReportGeneratorUnitTests { + + @TempDir Path tempDirectory; + + @Test + void generatesHtmlAndDiagramAssets() throws Exception { + + var modules = ApplicationModules.of(SampleApplication.class, new ImportOption.OnlyIncludeTests()); + var generator = new ModulithSiteReportGenerator(new PlantUmlSvgRenderer()); + var reportDirectory = tempDirectory.resolve("modulith"); + + generator.generate(modules, reportDirectory, DiagramStyle.UML, true); + + assertThat(reportDirectory.resolve("index.html")).exists(); + assertThat(reportDirectory.resolve("assets/components.puml")).exists(); + assertThat(reportDirectory.resolve("assets/module-order.puml")).exists(); + assertThat(reportDirectory.resolve("assets/module-inventory.puml")).exists(); + assertThat(reportDirectory.resolve("assets/components.svg")).exists(); + + String html = Files.readString(reportDirectory.resolve("index.html")); + assertThat(html).contains("Spring Modulith Report"); + assertThat(html).contains("assets/components.puml"); + assertThat(html).contains("assets/module-order.svg"); + assertThat(html).contains("Show PlantUML source"); + + String overview = Files.readString(reportDirectory.resolve("assets/components.puml")); + assertThat(overview).contains("Inventory"); + assertThat(overview).contains("Order"); + } + + @Test + void fallsBackToPlantUmlSourceIfSvgCannotBeRendered() throws Exception { + + var modules = ApplicationModules.of(SampleApplication.class, new ImportOption.OnlyIncludeTests()); + var generator = new ModulithSiteReportGenerator(source -> new RenderedDiagram(null, "boom")); + var reportDirectory = tempDirectory.resolve("fallback"); + + generator.generate(modules, reportDirectory, DiagramStyle.UML, true); + + assertThat(reportDirectory.resolve("assets/components.puml")).exists(); + assertThat(reportDirectory.resolve("assets/components.svg")).doesNotExist(); + assertThat(Files.readString(reportDirectory.resolve("index.html"))) + .contains("No SVG preview is available") + .contains("boom"); + } +} + + diff --git a/spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/sample/SampleApplication.java b/spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/sample/SampleApplication.java new file mode 100644 index 00000000..7b524d97 --- /dev/null +++ b/spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/sample/SampleApplication.java @@ -0,0 +1,8 @@ +package org.springframework.modulith.maven.sample; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleApplication {} + + diff --git a/spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/sample/inventory/InventoryManagement.java b/spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/sample/inventory/InventoryManagement.java new file mode 100644 index 00000000..fe67a65d --- /dev/null +++ b/spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/sample/inventory/InventoryManagement.java @@ -0,0 +1,12 @@ +package org.springframework.modulith.maven.sample.inventory; + +import org.springframework.stereotype.Service; + +@Service +public class InventoryManagement { + + public String reserve(String item) { + return item; + } +} + diff --git a/spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/sample/order/OrderManagement.java b/spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/sample/order/OrderManagement.java new file mode 100644 index 00000000..66d1aa98 --- /dev/null +++ b/spring-modulith-maven-plugin/src/test/java/org/springframework/modulith/maven/sample/order/OrderManagement.java @@ -0,0 +1,19 @@ +package org.springframework.modulith.maven.sample.order; + +import org.springframework.modulith.maven.sample.inventory.InventoryManagement; +import org.springframework.stereotype.Service; + +@Service +public class OrderManagement { + + private final InventoryManagement inventory; + + OrderManagement(InventoryManagement inventory) { + this.inventory = inventory; + } + + public String create(String item) { + return inventory.reserve(item); + } +} +