diff --git a/pom.xml b/pom.xml index 658d17801..5efd97eda 100644 --- a/pom.xml +++ b/pom.xml @@ -325,6 +325,12 @@ ${junit.version} test + + org.testcontainers + junit-jupiter + 1.17.6 + test + org.hamcrest hamcrest diff --git a/src/main/java/nl/talsmasoftware/umldoclet/configuration/Configuration.java b/src/main/java/nl/talsmasoftware/umldoclet/configuration/Configuration.java index c2ad885a8..c8e80d3ba 100644 --- a/src/main/java/nl/talsmasoftware/umldoclet/configuration/Configuration.java +++ b/src/main/java/nl/talsmasoftware/umldoclet/configuration/Configuration.java @@ -31,7 +31,26 @@ public interface Configuration { /** - * Tha name of the doclet to delegate main documentation to + * The base URL of the PlantUML server to generate diagrams with. + *

+ * Please note that it is not recommended to use the public, central PlantUML server at + * https://www.plantuml.com/plantuml. + * Although not strictly forbidden by the author of PlantUML, using the central server to generate your + * javadoc diagrams is causing additional load on the central server and is a lot slower than running your own + * local server. + *

+ * Using docker to run a local PlantUML server can be a simple as: + *

{@code
+     * docker run -d -p 8080:8080 plantuml/plantuml-server:latest
+     * }
+ * After that, you can run the UMLDoclet with {@code plantumlServerUrl = "http://localhost:8080/"} + * + * @return The base URL of the PlantUML online server to use. + */ + Optional plantumlServerUrl(); + + /** + * The name of the doclet to delegate main documentation to * or {@link Optional#empty} if no delegation is wanted. * * @return The name of the doclet to delegate main documentation to diff --git a/src/main/java/nl/talsmasoftware/umldoclet/javadoc/DocletConfig.java b/src/main/java/nl/talsmasoftware/umldoclet/javadoc/DocletConfig.java index ca400fc8a..85983f3a3 100644 --- a/src/main/java/nl/talsmasoftware/umldoclet/javadoc/DocletConfig.java +++ b/src/main/java/nl/talsmasoftware/umldoclet/javadoc/DocletConfig.java @@ -52,6 +52,8 @@ public class DocletConfig implements Configuration { private final UMLOptions options; private volatile LocalizedReporter reporter; + String plantumlServerUrl = null; + /** * The name of the delegate doclet to use for the main documentation task. *

@@ -139,6 +141,11 @@ public Set mergeOptionsWith(Set standard return options.mergeWith(standardOptions); } + @Override + public Optional plantumlServerUrl() { + return Optional.ofNullable(plantumlServerUrl); + } + @Override public Optional delegateDocletName() { return Optional.ofNullable(delegateDoclet).filter(name -> !"false".equalsIgnoreCase(name)); diff --git a/src/main/java/nl/talsmasoftware/umldoclet/javadoc/UMLOptions.java b/src/main/java/nl/talsmasoftware/umldoclet/javadoc/UMLOptions.java index 4c8ef7525..ed7da611d 100644 --- a/src/main/java/nl/talsmasoftware/umldoclet/javadoc/UMLOptions.java +++ b/src/main/java/nl/talsmasoftware/umldoclet/javadoc/UMLOptions.java @@ -80,6 +80,8 @@ private UMLOptions(DocletConfig config, Set standardOpt this.options.add(new Option("-d", 1, Kind.OTHER, args -> config.destDirName = args.get(0))); // Our own options + this.options.add(new Option("--plantuml-server-url -plantumlServerUrl", 1, Kind.STANDARD, + args -> config.plantumlServerUrl = args.get(0))); this.options.add(new Option("--delegate-doclet -delegateDoclet", 1, Kind.STANDARD, args -> config.delegateDoclet = args.get(0))); this.options.add(new Option("--create-puml-files -createPumlFiles", 0, Kind.STANDARD, args -> config.renderPumlFile = true)); diff --git a/src/main/java/nl/talsmasoftware/umldoclet/uml/Diagram.java b/src/main/java/nl/talsmasoftware/umldoclet/uml/Diagram.java index 6814a6375..517683e58 100644 --- a/src/main/java/nl/talsmasoftware/umldoclet/uml/Diagram.java +++ b/src/main/java/nl/talsmasoftware/umldoclet/uml/Diagram.java @@ -16,19 +16,18 @@ package nl.talsmasoftware.umldoclet.uml; import net.sourceforge.plantuml.FileFormat; -import net.sourceforge.plantuml.FileFormatOption; -import net.sourceforge.plantuml.SourceStringReader; import nl.talsmasoftware.umldoclet.configuration.Configuration; import nl.talsmasoftware.umldoclet.configuration.ImageConfig; import nl.talsmasoftware.umldoclet.logging.Message; import nl.talsmasoftware.umldoclet.rendering.indent.IndentingPrintWriter; import nl.talsmasoftware.umldoclet.rendering.writers.StringBufferingWriter; +import nl.talsmasoftware.umldoclet.uml.plantuml.PlantumlGenerator; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.nio.file.Files; import java.util.List; import java.util.Objects; import java.util.stream.Stream; @@ -40,16 +39,19 @@ import static nl.talsmasoftware.umldoclet.util.FileUtils.withoutExtension; /** - * Abstract class corresponding to a single UML diagram. + * Abstract UML Diagram class. */ public abstract class Diagram extends UMLNode { + private final Configuration config; + private final PlantumlGenerator plantumlGenerator; private final FileFormat[] formats; private File diagramBaseFile; protected Diagram(Configuration config) { super(null); this.config = requireNonNull(config, "Configuration is "); + this.plantumlGenerator = PlantumlGenerator.getPlantumlGenerator(config); this.formats = config.images().formats().stream() .map(this::toFileFormat).filter(Objects::nonNull) .toArray(FileFormat[]::new); @@ -90,7 +92,7 @@ public Configuration getConfiguration() { * Determine the physical file location for the plantuml output. * *

This will even be called if {@code -createPumlFiles} is not enabled, - * to determine the {@linkplain #getDiagramBaseFile()}. + * to determine the {@code diagram base file}. * * @return The physical file for the plantuml output. */ @@ -167,15 +169,15 @@ private String writePlantumlSourceToFile() throws IOException { private StringBufferingWriter createBufferingPlantumlFileWriter(File pumlFile) throws IOException { return new StringBufferingWriter( new OutputStreamWriter( - new FileOutputStream(pumlFile), config.umlCharset())); + Files.newOutputStream(pumlFile.toPath()), config.umlCharset())); } private void renderDiagramFile(String plantumlSource, FileFormat format) throws IOException { final File diagramFile = getDiagramFile(format); config.logger().info(Message.INFO_GENERATING_FILE, diagramFile); ensureParentDir(diagramFile); - try (OutputStream out = new FileOutputStream(diagramFile)) { - new SourceStringReader(plantumlSource).outputImage(out, new FileFormatOption(format)); + try (OutputStream out = Files.newOutputStream(diagramFile.toPath())) { + plantumlGenerator.generatePlantumlDiagramFromSource(plantumlSource, format, out); } } diff --git a/src/main/java/nl/talsmasoftware/umldoclet/uml/plantuml/BuiltinPlantumlGenerator.java b/src/main/java/nl/talsmasoftware/umldoclet/uml/plantuml/BuiltinPlantumlGenerator.java new file mode 100644 index 000000000..3c251e2bb --- /dev/null +++ b/src/main/java/nl/talsmasoftware/umldoclet/uml/plantuml/BuiltinPlantumlGenerator.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016-2022 Talsma ICT + * + * 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 + * + * http://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 nl.talsmasoftware.umldoclet.uml.plantuml; + +import net.sourceforge.plantuml.FileFormat; +import net.sourceforge.plantuml.FileFormatOption; +import net.sourceforge.plantuml.SourceStringReader; + +import java.io.IOException; +import java.io.OutputStream; + +public final class BuiltinPlantumlGenerator implements PlantumlGenerator { + @Override + public void generatePlantumlDiagramFromSource(String plantumlSource, FileFormat format, OutputStream out) throws IOException { + new SourceStringReader(plantumlSource).outputImage(out, new FileFormatOption(format)); + } +} diff --git a/src/main/java/nl/talsmasoftware/umldoclet/uml/plantuml/PlantumlGenerator.java b/src/main/java/nl/talsmasoftware/umldoclet/uml/plantuml/PlantumlGenerator.java new file mode 100644 index 000000000..ae82784de --- /dev/null +++ b/src/main/java/nl/talsmasoftware/umldoclet/uml/plantuml/PlantumlGenerator.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016-2022 Talsma ICT + * + * 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 + * + * http://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 nl.talsmasoftware.umldoclet.uml.plantuml; + +import net.sourceforge.plantuml.FileFormat; +import nl.talsmasoftware.umldoclet.configuration.Configuration; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.regex.Pattern; + +public interface PlantumlGenerator { + Pattern HTTP_URLS = Pattern.compile("^https?://"); + + static PlantumlGenerator getPlantumlGenerator(Configuration configuration) { + return configuration.plantumlServerUrl() + .filter(url -> HTTP_URLS.matcher(url).find()) + .map(url -> (PlantumlGenerator) new RemotePlantumlGenerator(url)) + .orElseGet(BuiltinPlantumlGenerator::new); + } + + void generatePlantumlDiagramFromSource(String plantumlSource, FileFormat format, OutputStream out) throws IOException; + +} diff --git a/src/main/java/nl/talsmasoftware/umldoclet/uml/plantuml/RemotePlantumlGenerator.java b/src/main/java/nl/talsmasoftware/umldoclet/uml/plantuml/RemotePlantumlGenerator.java new file mode 100644 index 000000000..491c1825c --- /dev/null +++ b/src/main/java/nl/talsmasoftware/umldoclet/uml/plantuml/RemotePlantumlGenerator.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016-2022 Talsma ICT + * + * 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 + * + * http://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 nl.talsmasoftware.umldoclet.uml.plantuml; + +import net.sourceforge.plantuml.FileFormat; +import net.sourceforge.plantuml.code.ArobaseStringCompressor; +import net.sourceforge.plantuml.code.AsciiEncoder; +import net.sourceforge.plantuml.code.CompressionZlib; +import net.sourceforge.plantuml.code.Transcoder; +import net.sourceforge.plantuml.code.TranscoderImpl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public class RemotePlantumlGenerator implements PlantumlGenerator { + private static final String DEFAULT_PLANTUML_BASE_URL = "https://www.plantuml.com/plantuml/"; + private static final Transcoder TRANSCODER = + TranscoderImpl.utf8(new AsciiEncoder(), new ArobaseStringCompressor(), new CompressionZlib()); + + private final String baseUrl; + + public RemotePlantumlGenerator(final String baseUrl) { + String url = Objects.toString(baseUrl, DEFAULT_PLANTUML_BASE_URL); + if (!url.endsWith("/")) url += "/"; + this.baseUrl = url; + } + + @Override + public void generatePlantumlDiagramFromSource(String plantumlSource, FileFormat format, OutputStream out) { + final String encodedDiagram = encodeDiagram(plantumlSource); + final String diagramUrl = baseUrl + format.name().toLowerCase() + '/' + encodedDiagram; + try (InputStream in = new URL(diagramUrl).openConnection().getInputStream()) { + final byte[] buf = new byte[4096]; + for (int read = in.read(buf); read >= 0; read = in.read(buf)) { + out.write(buf, 0, read); + } + } catch (IOException | RuntimeException e) { + throw new RuntimeException(e); + } + } + + private String encodeDiagram(final String diagramSource) { + try { + // TODO internalize transcoder to be able to remove PlantUML dependency altogether. + return TRANSCODER.encode(requireNonNull(diagramSource, "UML diagram source was .")); + } catch (IOException ioe) { + throw new IllegalStateException("Error encoding diagram: " + ioe.getMessage(), ioe); + } + } + +} diff --git a/src/main/resources/nl/talsmasoftware/umldoclet/UMLDoclet.properties b/src/main/resources/nl/talsmasoftware/umldoclet/UMLDoclet.properties index 70377b444..5312763b5 100644 --- a/src/main/resources/nl/talsmasoftware/umldoclet/UMLDoclet.properties +++ b/src/main/resources/nl/talsmasoftware/umldoclet/UMLDoclet.properties @@ -23,6 +23,8 @@ error.unanticipated.error.generating.diagrams=Unanticipated error generating dia error.unanticipated.error.postprocessing.html=Unanticipated error post-processing HTML: {0} # Usage +doclet.usage.plantuml-server-url.description=Base URL for the PlantUML server\nExamples: http://localhost:8080/, https://www.plantuml.com/plantuml/ +doclet.usage.plantuml-server-url.parameters= doclet.usage.delegate-doclet.description=The delegate doclet providing the main documentation\nDefaults to 'jdk.javadoc.doclet.StandardDoclet'\nSpecify 'false' to disable delegation doclet.usage.delegate-doclet.parameters= doclet.usage.create-puml-files.description=Create PlantUML '.puml' files diff --git a/src/test/java/nl/talsmasoftware/umldoclet/uml/DependencyDiagramTest.java b/src/test/java/nl/talsmasoftware/umldoclet/uml/DependencyDiagramTest.java index 7673b0879..70a32d599 100644 --- a/src/test/java/nl/talsmasoftware/umldoclet/uml/DependencyDiagramTest.java +++ b/src/test/java/nl/talsmasoftware/umldoclet/uml/DependencyDiagramTest.java @@ -30,8 +30,17 @@ import static java.util.Collections.singleton; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.*; -import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; public class DependencyDiagramTest { private ImageConfig mockImages; @@ -54,6 +63,7 @@ public void prepareMocksDiagramAndExclusions() { public void verifyMocks() { verify(mockConfig, atLeastOnce()).images(); verify(mockImages, atLeastOnce()).formats(); + verify(mockConfig, atLeast(0)).plantumlServerUrl(); verify(mockConfig, atLeast(0)).excludedPackageDependencies(); verify(mockConfig, atLeast(0)).indentation(); verifyNoMoreInteractions(mockConfig, mockImages); diff --git a/src/test/java/nl/talsmasoftware/umldoclet/uml/DiagramTest.java b/src/test/java/nl/talsmasoftware/umldoclet/uml/DiagramTest.java index 8e11b618f..94643b1b9 100644 --- a/src/test/java/nl/talsmasoftware/umldoclet/uml/DiagramTest.java +++ b/src/test/java/nl/talsmasoftware/umldoclet/uml/DiagramTest.java @@ -71,6 +71,7 @@ public void setUp() { @AfterEach public void tearDown() { + verify(config, atLeast(0)).plantumlServerUrl(); verify(config, atLeast(0)).images(); verify(config, atLeast(0)).destinationDirectory(); verify(config, atLeast(0)).logger(); diff --git a/src/test/java/nl/talsmasoftware/umldoclet/uml/NamespaceTest.java b/src/test/java/nl/talsmasoftware/umldoclet/uml/NamespaceTest.java index b00c11e3b..28035d9d5 100644 --- a/src/test/java/nl/talsmasoftware/umldoclet/uml/NamespaceTest.java +++ b/src/test/java/nl/talsmasoftware/umldoclet/uml/NamespaceTest.java @@ -52,10 +52,12 @@ public void verifyMocks() { public void testEquals() { PackageDiagram packageUml = new PackageDiagram(config, "a.b.c", randomString()); Namespace namespace = new Namespace(packageUml, "a.b.c", randomString()); + assertThat(namespace.equals(namespace), is(true)); assertThat(namespace, is(equalTo(new Namespace(null, "a.b.c", randomString())))); assertThat(namespace, is(equalTo(new Namespace(packageUml, "a.b.c", randomString())))); assertThat(namespace, is(not(equalTo(new Namespace(packageUml, "A.B.C", randomString()))))); + verify(config, atLeastOnce()).plantumlServerUrl(); } } diff --git a/src/test/java/nl/talsmasoftware/umldoclet/uml/plantuml/RemotePlantumlGeneratorTest.java b/src/test/java/nl/talsmasoftware/umldoclet/uml/plantuml/RemotePlantumlGeneratorTest.java new file mode 100644 index 000000000..3529aa3e7 --- /dev/null +++ b/src/test/java/nl/talsmasoftware/umldoclet/uml/plantuml/RemotePlantumlGeneratorTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016-2022 Talsma ICT + * + * 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 + * + * http://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 nl.talsmasoftware.umldoclet.uml.plantuml; + +import net.sourceforge.plantuml.FileFormat; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@Testcontainers +class RemotePlantumlGeneratorTest { + static final String testUml = "@startuml\r\nBob -> Alice : hello\r\n@enduml"; + + @Container + GenericContainer plantumlServer = new GenericContainer(DockerImageName.parse("plantuml/plantuml-server")) + .withExposedPorts(8080); + + PlantumlGenerator subject; + + @BeforeEach + void setUp() { + subject = new RemotePlantumlGenerator(String.format("http://%s:%s/", + plantumlServer.getHost(), plantumlServer.getMappedPort(8080))); +// subject = new RemotePlantumlGenerator("https://www.plantuml.com/plantuml/"); +// subject = new RemotePlantumlGenerator("http://localhost:8080/"); + } + + @Test + void simpleDiagramCanBeGenerated() throws IOException { + // prepare + final File testDiagram = new File("target/test-classes/" + + getClass().getPackageName().replace('.', '/') + + "/testUml.svg"); + testDiagram.delete(); + + // execute + try (OutputStream out = new FileOutputStream(testDiagram)) { + subject.generatePlantumlDiagramFromSource(testUml, FileFormat.SVG, out); + } + + // verify + assertThat(testDiagram.isFile(), is(true)); + } + +} diff --git a/src/test/resources/logback.xml b/src/test/resources/logback-test.xml similarity index 96% rename from src/test/resources/logback.xml rename to src/test/resources/logback-test.xml index 5c2c51bd4..5131db2ad 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback-test.xml @@ -1,7 +1,6 @@ - true