diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java index be9a9d451..9c68e13a0 100644 --- a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java +++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java @@ -20,7 +20,6 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.io.Writer; import java.lang.annotation.Annotation; import java.nio.file.Files; import java.nio.file.Path; @@ -71,6 +70,7 @@ * * @author Oliver Drotbohm * @author Cora Iberkleid + * @author Tobias Haindl */ public class Documenter { @@ -91,6 +91,8 @@ public class Documenter { private final ConfigurationProperties properties; private final Options options; + private boolean cleared; + private Map components; /** @@ -113,9 +115,17 @@ public Documenter(ApplicationModules modules) { this(modules, Options.defaults()); } + /** + * Creates a new {@link Documenter} for the given {@link ApplicationModules} and output folder. + * + * @param modules must not be {@literal null}. + * @param outputFolder must not be {@literal null} or empty. + * @deprecated use {@link Documenter(ApplicationModules, Options)} instead. + */ + @Deprecated(forRemoval = true) public Documenter(ApplicationModules modules, String outputFolder) { - this(modules, new Options(outputFolder, true)); + this(modules, Options.defaults().withOutputFolder(outputFolder)); Assert.hasText(outputFolder, "Output folder must not be null or empty!"); } @@ -125,6 +135,7 @@ public Documenter(ApplicationModules modules, String outputFolder) { * * @param modules must not be {@literal null}. * @param options must not be {@literal null}. + * @since 1.2 */ public Documenter(ApplicationModules modules, Options options) { @@ -146,10 +157,12 @@ public Documenter(ApplicationModules modules, Options options) { this.container = system.addContainer(systemName, "", ""); this.properties = new ConfigurationProperties(); + this.cleared = false; } /** - * Customize the output folder to write the generated files to. Defaults to {@value #DEFAULT_LOCATION}. + * Customize the output folder to write the generated files to. Defaults to {@code spring-modulith-docs} in your build + * systems build folder. * * @param outputFolder must not be {@literal null} or empty. * @return will never be {@literal null}. @@ -189,14 +202,12 @@ public Documenter writeDocumentation() { */ public Documenter writeDocumentation(DiagramOptions diagramOptions, CanvasOptions canvasOptions) { - if (this.options.clean) { - clearOutputFolder(); - } + potentiallyWipeOutputFolder(); - return writeModulesAsPlantUml(options) - .writeIndividualModulesAsPlantUml(options) + return writeModulesAsPlantUml(diagramOptions) + .writeIndividualModulesAsPlantUml(diagramOptions) .writeModuleCanvases(canvasOptions) - .writeAggregatingDocument(options, canvasOptions); + .writeAggregatingDocument(diagramOptions, canvasOptions); } /** @@ -211,26 +222,29 @@ public Documenter writeAggregatingDocument() { } /** - * Writes aggregating document called 'all-docs.adoc' that includes any existing component diagrams and canvases. + * Writes aggregating document called {@code all-docs.adoc} that includes any existing component diagrams and + * canvases. * - * @param options must not be {@literal null}. + * @param diagramOptions must not be {@literal null}. * @param canvasOptions must not be {@literal null}. * @return the current instance, will never be {@literal null}. * @since 1.2.2 */ - public Documenter writeAggregatingDocument(DiagramOptions options, CanvasOptions canvasOptions) { + public Documenter writeAggregatingDocument(DiagramOptions diagramOptions, CanvasOptions canvasOptions) { - Assert.notNull(options, "DiagramOptions must not be null!"); + Assert.notNull(diagramOptions, "DiagramOptions must not be null!"); Assert.notNull(canvasOptions, "CanvasOptions must not be null!"); + potentiallyWipeOutputFolder(); + var asciidoctor = Asciidoctor.withJavadocBase(modules, canvasOptions.getApiBase()); - var outputFolder = new OutputFolder(this.outputFolder); // Get file name for module overview diagram - var componentsFilename = options.getTargetFileName().orElse(DEFAULT_COMPONENTS_FILE); + var componentsFilename = diagramOptions.getTargetFileName().orElse(DEFAULT_COMPONENTS_FILE); var componentsDoc = new StringBuilder(); + var folder = options.outputFolder; - if (outputFolder.contains(componentsFilename)) { + if (folder.contains(componentsFilename)) { componentsDoc .append(asciidoctor.renderHeadline(2, getDefaultedSystemName())) @@ -242,13 +256,13 @@ public Documenter writeAggregatingDocument(DiagramOptions options, CanvasOptions var moduleDocs = modules.stream().map(it -> { // Get diagram file name, e.g. module-inventory.puml - var fileNamePattern = options.getTargetFileName().orElse(DEFAULT_MODULE_COMPONENTS_FILE); + var fileNamePattern = diagramOptions.getTargetFileName().orElse(DEFAULT_MODULE_COMPONENTS_FILE); var filename = fileNamePattern.formatted(it.getName()); var canvasFilename = canvasOptions.getTargetFileName(it.getName()); var content = new StringBuilder(); - content.append(outputFolder.contains(filename) ? asciidoctor.renderPlantUmlInclude(filename) : "") - .append(outputFolder.contains(canvasFilename) ? asciidoctor.renderGeneralInclude(canvasFilename) : ""); + content.append(folder.contains(filename) ? asciidoctor.renderPlantUmlInclude(filename) : "") + .append(folder.contains(canvasFilename) ? asciidoctor.renderGeneralInclude(canvasFilename) : ""); if (!content.isEmpty()) { @@ -265,14 +279,7 @@ public Documenter writeAggregatingDocument(DiagramOptions options, CanvasOptions // Write file to all-docs.adoc if (!allDocs.isBlank()) { - - var file = recreateFile("all-docs.adoc"); - - try (Writer writer = new FileWriter(file.toFile())) { - writer.write(allDocs); - } catch (IOException o_O) { - throw new RuntimeException(o_O); - } + options.outputFolder.writeToFile("all-docs.adoc", allDocs); } return this; @@ -290,20 +297,17 @@ public Documenter writeModulesAsPlantUml() { /** * Writes the PlantUML component diagram for all {@link ApplicationModules} with the given {@link DiagramOptions}. * - * @param options must not be {@literal null}. + * @param diagramOptions must not be {@literal null}. * @return the current instance, will never be {@literal null}. */ - public Documenter writeModulesAsPlantUml(DiagramOptions options) { + public Documenter writeModulesAsPlantUml(DiagramOptions diagramOptions) { - Assert.notNull(options, "Options must not be null!"); + Assert.notNull(diagramOptions, "Options must not be null!"); - Path file = recreateFile(options.getTargetFileName().orElse(DEFAULT_COMPONENTS_FILE)); + potentiallyWipeOutputFolder(); - try (Writer writer = new FileWriter(file.toFile())) { - writer.write(createPlantUml(options)); - } catch (IOException o_O) { - throw new RuntimeException(o_O); - } + options.outputFolder.writeToFile(diagramOptions.getTargetFileName().orElse(DEFAULT_COMPONENTS_FILE), + createPlantUml(diagramOptions)); return this; } @@ -322,6 +326,8 @@ public Documenter writeIndividualModulesAsPlantUml(DiagramOptions options) { Assert.notNull(options, "DiagramOptions must not be null!"); + potentiallyWipeOutputFolder(); + modules.forEach(it -> writeModuleAsPlantUml(it, options)); return this; @@ -353,6 +359,8 @@ public Documenter writeModuleAsPlantUml(ApplicationModule module, DiagramOptions Assert.notNull(module, "Module must not be null!"); Assert.notNull(options, "Options must not be null!"); + potentiallyWipeOutputFolder(); + var view = createComponentView(options, module); view.setTitle(options.defaultDisplayName.apply(module)); @@ -375,25 +383,20 @@ public Documenter writeModuleCanvases() { /** * Writes all module canvases using the given {@link DiagramOptions}. * - * @param options must not be {@literal null}. + * @param canvasOptions must not be {@literal null}. * @return the current instance, will never be {@literal null}. */ - public Documenter writeModuleCanvases(CanvasOptions options) { - - Assert.notNull(options, "CanvasOptions must not be null!"); + public Documenter writeModuleCanvases(CanvasOptions canvasOptions) { - modules.forEach(module -> { + Assert.notNull(canvasOptions, "CanvasOptions must not be null!"); - var filename = options.getTargetFileName(module.getName()); - var file = recreateFile(filename); + potentiallyWipeOutputFolder(); - try (FileWriter writer = new FileWriter(file.toFile())) { + modules.forEach(module -> { - writer.write(toModuleCanvas(module, options)); + var filename = canvasOptions.getTargetFileName(module.getName()); - } catch (IOException o_O) { - throw new RuntimeException(o_O); - } + options.outputFolder.writeToFile(filename, toModuleCanvas(module, canvasOptions)); }); return this; @@ -552,19 +555,11 @@ private void potentiallyRemoveDefaultRelationship(ModelView view, Collection paths = Files.walk(outputPath)) { - paths.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); - } catch (IOException o_O) { - throw new RuntimeException(o_O); - } - } - - private Path recreateFile(String name) { - - try { - - var outputFolder = options.outputFolder; + options.outputFolder.deleteIfExists(); - Files.createDirectories(Paths.get(outputFolder)); - Path filePath = Paths.get(outputFolder, name); - Files.deleteIfExists(filePath); - - return Files.createFile(filePath); - - } catch (IOException o_O) { - throw new RuntimeException(o_O); + this.cleared = true; } } @@ -678,15 +652,6 @@ private static String addTableRow(List types, String header, Function { - doWith(path, it -> { - - new Documenter(ApplicationModules.of(Application.class), customOutputFolder).writeModuleCanvases(); + documenter.writeModuleCanvases(); assertThat(Files.list(path)).isNotEmpty(); assertThat(path).exists(); @@ -94,22 +91,20 @@ void customizesOutputLocation() throws Exception { @Test // GH-638 void createsAggregatingDocumentOnlyIfPartialsExist() throws Exception { - var customOutputFolder = "build/spring-modulith"; - var path = Paths.get(customOutputFolder); - var documenter = new Documenter(ApplicationModules.of(Application.class), customOutputFolder); - - doWith(path, it -> { + doWith("build/spring-modulith", (path, documenter) -> { // all-docs.adoc should be created documenter.writeDocumentation(); + var numberOfModules = documenter.getModules().stream().count(); + // 2 per module (PlantUML + Canvas) + component overview + aggregating doc - var expectedFiles = documenter.getModules().stream().count() * 2 + 2; + var expectedFiles = numberOfModules * 2 + 2; // 3 per module (headline + PlantUML + Canvas) + component headline + component PlantUML - var expectedLines = documenter.getModules().stream().count() * 3 + 2; + var expectedLines = numberOfModules * 3 + 2; - assertThat(Files.walk(it).filter(Files::isRegularFile).count()) + assertThat(Files.walk(path).filter(Files::isRegularFile).count()) .isEqualTo(expectedFiles); assertThat(path.resolve("all-docs.adoc")).exists().satisfies(doc -> { @@ -123,15 +118,11 @@ void createsAggregatingDocumentOnlyIfPartialsExist() throws Exception { @Test // GH-638 void doesNotCreateAggregatingDocumentIfNoPartialsExist() throws Exception { - var customOutputFolder = "build/spring-modulith"; - var path = Paths.get(customOutputFolder); + doWith("build/spring-modulith", (path, documenter) -> { - doWith(path, it -> { - - var documenter = new Documenter(ApplicationModules.of(Application.class), customOutputFolder) - .writeDocumentation(); + documenter.writeDocumentation(); - deleteDirectoryContents(it); + deleteDirectoryContents(path); documenter.writeAggregatingDocument(); @@ -141,32 +132,37 @@ void doesNotCreateAggregatingDocumentIfNoPartialsExist() throws Exception { }); } - @Test - void shouldCleanOutputLocation(@TempDir Path outputDirectory) throws IOException { + @Test // GH-644 + void cleansOutputDirectoryByDefault(@TempDir Path outputDirectory) { - var filePath = createTestFile(outputDirectory); - var nestedFiledPath = createTestFileInSubdirectory(outputDirectory); + doWith(outputDirectory.toString(), (path, documenter) -> { - new Documenter(ApplicationModules.of(Application.class), outputDirectory.toString()).writeDocumentation(); + var filePath = createTestFile(path); + var nestedFiledPath = createTestFileInSubdirectory(path); + + documenter.writeDocumentation(); + + assertThat(filePath).doesNotExist(); + assertThat(nestedFiledPath).doesNotExist(); + assertThat(Files.list(path)).isNotEmpty(); + }); - assertThat(filePath).doesNotExist(); - assertThat(nestedFiledPath).doesNotExist(); - assertThat(Files.list(outputDirectory)).isNotEmpty(); } - @Test - void shouldNotCleanOutputLocation(@TempDir Path outputDirectory) throws IOException { - - var filePath = createTestFile(outputDirectory); - var nestedFiledPath = createTestFileInSubdirectory(outputDirectory); - - new Documenter(ApplicationModules.of(Application.class), - Options.defaults().withOutputFolder(outputDirectory.toString()).withoutClean()) - .writeDocumentation(); - - assertThat(filePath).exists(); - assertThat(nestedFiledPath).exists(); - assertThat(Files.list(outputDirectory)).isNotEmpty(); + @Test // GH-644 + void doesNotCleanOutputDirectoryIfConfigured(@TempDir Path outputDirectory) throws IOException { + + doWith(outputDirectory.toString(), it -> it.withoutClean(), (path, documenter) -> { + + var filePath = createTestFile(path); + var nestedFiledPath = createTestFileInSubdirectory(path); + + documenter.writeDocumentation(); + + assertThat(filePath).exists(); + assertThat(nestedFiledPath).exists(); + assertThat(Files.list(path)).isNotEmpty(); + }); } private static Path createTestFile(Path tempDir) throws IOException { @@ -193,18 +189,26 @@ private static void deleteDirectoryContents(Path path) throws IOException { } } - private static void deleteDirectory(Path path) throws IOException { - - deleteDirectoryContents(path); - Files.deleteIfExists(path); + private static void doWith(String path, ThrowingBiConsumer consumer) { + doWith(path, Function.identity(), consumer); } - private static void doWith(Path path, ThrowingConsumer consumer) throws Exception { + private static void doWith(String path, Function customizer, + ThrowingBiConsumer consumer) { + + var options = customizer.apply(Options.defaults().withOutputFolder(path)); + var modules = ApplicationModules.of(Application.class); try { - consumer.accept(path); + consumer.accept(Path.of(path), new Documenter(modules, options)); + } catch (Exception o_O) { + throw new RuntimeException(o_O); } finally { - deleteDirectory(path); + options.getOutputFolder().deleteIfExists(); } } + + private interface ThrowingBiConsumer { + void accept(T t, S s) throws Exception; + } }