From 4719feaf0a5072b977590b3c389f59e9cd4ae18e Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Thu, 6 Jul 2023 16:43:28 +0100 Subject: [PATCH] Adds some debug to help diagnose problems with running Graphviz. --- build.gradle | 3 +- docs/changelog.md | 4 + .../graphviz/GraphvizAutomaticLayout.java | 43 ++- .../com/structurizr/graphviz/SVGReader.java | 253 +++++++++--------- 4 files changed, 180 insertions(+), 123 deletions(-) diff --git a/build.gradle b/build.gradle index 3e0ac79..494eada 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ repositories { dependencies { api 'com.structurizr:structurizr-export:1.14.0' + implementation 'org.apache.logging.log4j:log4j-api:2.17.1' testImplementation 'com.structurizr:structurizr-client:1.24.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' @@ -23,7 +24,7 @@ targetCompatibility = 1.8 description = 'Automatic layout facilities for Structurizr views' group = 'com.structurizr' -version = '2.0.1' +version = '2.1.0' test { useJUnitPlatform() diff --git a/docs/changelog.md b/docs/changelog.md index f6b23f1..3ee0eed 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 2.1.0 (6th July 2023) + +- Adds some debug to help diagnose problems with running Graphviz. + ## 2.0.1 (5th June 2023) - Fixes an issue with some older client libraries not setting the rank direction. diff --git a/src/main/java/com/structurizr/graphviz/GraphvizAutomaticLayout.java b/src/main/java/com/structurizr/graphviz/GraphvizAutomaticLayout.java index c7b9034..defdc86 100644 --- a/src/main/java/com/structurizr/graphviz/GraphvizAutomaticLayout.java +++ b/src/main/java/com/structurizr/graphviz/GraphvizAutomaticLayout.java @@ -3,11 +3,14 @@ import com.structurizr.Workspace; import com.structurizr.export.Diagram; import com.structurizr.view.*; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import java.io.BufferedWriter; import java.io.File; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.List; import java.util.Locale; /** @@ -17,6 +20,11 @@ */ public class GraphvizAutomaticLayout { + private static final Log log = LogFactory.getLog(GraphvizAutomaticLayout.class); + + private static final String DOT_EXECUTABLE = "dot"; + private static final String USE_SVG_OUTPUT_FORMAT_OPTION = "-Tsvg"; + private static final String AUTOMATICALLY_GENERATE_OUTPUT_FILE_OPTION = "-O"; private static final String DOT_FILE_EXTENSION = ".dot"; private final File path; @@ -76,10 +84,15 @@ private DOTExporter createDOTExporter() { private void writeFile(Diagram diagram) throws Exception { File file = new File(path, diagram.getKey() + DOT_FILE_EXTENSION); + log.debug("Writing " + file.getAbsolutePath()); BufferedWriter writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8); writer.write(diagram.getDefinition()); writer.flush(); writer.close(); + + if (!file.exists()) { + log.error(file.getAbsolutePath() + " does not exist"); + } } private SVGReader createSVGReader() { @@ -88,13 +101,35 @@ private SVGReader createSVGReader() { private void runGraphviz(View view) throws Exception { ProcessBuilder processBuilder = new ProcessBuilder().inheritIO(); - processBuilder.command("dot", new File(path, view.getKey() + DOT_FILE_EXTENSION).getAbsolutePath(), "-Tsvg", "-O"); + List command = List.of( + DOT_EXECUTABLE, + new File(path, view.getKey() + DOT_FILE_EXTENSION).getAbsolutePath(), + USE_SVG_OUTPUT_FORMAT_OPTION, + AUTOMATICALLY_GENERATE_OUTPUT_FILE_OPTION + ); + + processBuilder.command(command); + + StringBuilder buf = new StringBuilder(); + for (String s : command) { + buf.append(s); + buf.append(" "); + } + log.debug(buf); + Process process = processBuilder.start(); int exitCode = process.waitFor(); assert exitCode == 0; + + String input = new String(process.getInputStream().readAllBytes()); + String error = new String(process.getErrorStream().readAllBytes()); + + log.debug("stdout: " + input); + log.debug("stderr: " + error); } public void apply(CustomView view) throws Exception { + log.debug("Running Graphviz for view with key " + view.getKey()); Diagram diagram = createDOTExporter().export(view); writeFile(diagram); runGraphviz(view); @@ -102,6 +137,7 @@ public void apply(CustomView view) throws Exception { } public void apply(SystemLandscapeView view) throws Exception { + log.debug("Running Graphviz for view with key " + view.getKey()); Diagram diagram = createDOTExporter().export(view); writeFile(diagram); runGraphviz(view); @@ -109,6 +145,7 @@ public void apply(SystemLandscapeView view) throws Exception { } public void apply(SystemContextView view) throws Exception { + log.debug("Running Graphviz for view with key " + view.getKey()); Diagram diagram = createDOTExporter().export(view); writeFile(diagram); runGraphviz(view); @@ -116,6 +153,7 @@ public void apply(SystemContextView view) throws Exception { } public void apply(ContainerView view) throws Exception { + log.debug("Running Graphviz for view with key " + view.getKey()); Diagram diagram = createDOTExporter().export(view); writeFile(diagram); runGraphviz(view); @@ -123,6 +161,7 @@ public void apply(ContainerView view) throws Exception { } public void apply(ComponentView view) throws Exception { + log.debug("Running Graphviz for view with key " + view.getKey()); Diagram diagram = createDOTExporter().export(view); writeFile(diagram); runGraphviz(view); @@ -130,6 +169,7 @@ public void apply(ComponentView view) throws Exception { } public void apply(DynamicView view) throws Exception { + log.debug("Running Graphviz for view with key " + view.getKey()); Diagram diagram = createDOTExporter().export(view); writeFile(diagram); runGraphviz(view); @@ -137,6 +177,7 @@ public void apply(DynamicView view) throws Exception { } public void apply(DeploymentView view) throws Exception { + log.debug("Running Graphviz for view with key " + view.getKey()); Diagram diagram = createDOTExporter().export(view); writeFile(diagram); runGraphviz(view); diff --git a/src/main/java/com/structurizr/graphviz/SVGReader.java b/src/main/java/com/structurizr/graphviz/SVGReader.java index 026a821..4fde3f2 100644 --- a/src/main/java/com/structurizr/graphviz/SVGReader.java +++ b/src/main/java/com/structurizr/graphviz/SVGReader.java @@ -3,6 +3,8 @@ import com.structurizr.model.DeploymentNode; import com.structurizr.model.Element; import com.structurizr.view.*; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.w3c.dom.Document; import org.w3c.dom.NodeList; @@ -21,10 +23,11 @@ */ class SVGReader { - private File path; - private boolean changePaperSize; + private static final Log log = LogFactory.getLog(GraphvizAutomaticLayout.class); - private int margin; + private final File path; + private final int margin; + private final boolean changePaperSize; SVGReader(File path, int margin, boolean changePaperSize) { this.path = path; @@ -34,148 +37,156 @@ class SVGReader { void parseAndApplyLayout(ModelView view) throws Exception { File file = new File(path, view.getKey() + ".dot.svg"); - FileInputStream fileIS = new FileInputStream(file); - DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); - builderFactory.setNamespaceAware(false); - builderFactory.setValidating(false); - builderFactory.setFeature("http://xml.org/sax/features/namespaces", false); - builderFactory.setFeature("http://xml.org/sax/features/validation", false); - builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); - builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); - - DocumentBuilder builder = builderFactory.newDocumentBuilder(); - Document xmlDocument = builder.parse(fileIS); - XPath xPath = XPathFactory.newInstance().newXPath(); - NodeList nodeList = (NodeList)xPath.compile("/svg/g[@class=\"graph\"]").evaluate(xmlDocument, XPathConstants.NODESET); - String transform = nodeList.item(0).getAttributes().getNamedItem("transform").getNodeValue(); - String translate = transform.substring(transform.indexOf("translate")); - String numbers = translate.substring(translate.indexOf("(") + 1, translate.indexOf(")")); - int transformX = (int)Double.parseDouble(numbers.split(" ")[0]); - int transformY = (int)Double.parseDouble(numbers.split(" ")[1]); - - int minimumX = Integer.MAX_VALUE; - int minimumY = Integer.MAX_VALUE; - int maximumX = Integer.MIN_VALUE; - int maximumY = Integer.MIN_VALUE; - - for (ElementView elementView : view.getElements()) { - if (elementView.getElement() instanceof DeploymentNode) { - // deployment nodes are clusters, so positioned automatically - continue; - } - - String expression = String.format("/svg/g/g[@id=\"%s\"]/polygon", elementView.getId()); - nodeList = (NodeList)xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); - if (nodeList.getLength() == 0) { - continue; - } + log.debug("Reading " + file.getAbsolutePath()); + + if (file.exists()) { + FileInputStream fileIS = new FileInputStream(file); + DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); + builderFactory.setNamespaceAware(false); + builderFactory.setValidating(false); + builderFactory.setFeature("http://xml.org/sax/features/namespaces", false); + builderFactory.setFeature("http://xml.org/sax/features/validation", false); + builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); + builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + + DocumentBuilder builder = builderFactory.newDocumentBuilder(); + Document xmlDocument = builder.parse(fileIS); + XPath xPath = XPathFactory.newInstance().newXPath(); + NodeList nodeList = (NodeList) xPath.compile("/svg/g[@class=\"graph\"]").evaluate(xmlDocument, XPathConstants.NODESET); + String transform = nodeList.item(0).getAttributes().getNamedItem("transform").getNodeValue(); + String translate = transform.substring(transform.indexOf("translate")); + String numbers = translate.substring(translate.indexOf("(") + 1, translate.indexOf(")")); + int transformX = (int) Double.parseDouble(numbers.split(" ")[0]); + int transformY = (int) Double.parseDouble(numbers.split(" ")[1]); + + int minimumX = Integer.MAX_VALUE; + int minimumY = Integer.MAX_VALUE; + int maximumX = Integer.MIN_VALUE; + int maximumY = Integer.MIN_VALUE; + + for (ElementView elementView : view.getElements()) { + if (elementView.getElement() instanceof DeploymentNode) { + // deployment nodes are clusters, so positioned automatically + continue; + } - String pointsAsString = nodeList.item(0).getAttributes().getNamedItem("points").getNodeValue(); - String[] points = pointsAsString.split(" "); - String[] coordinates = points[1].split(","); + String expression = String.format("/svg/g/g[@id=\"%s\"]/polygon", elementView.getId()); + nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); + if (nodeList.getLength() == 0) { + continue; + } - double x = Double.parseDouble(coordinates[0]) + transformX; - double y = Double.parseDouble(coordinates[1]) + transformY; + String pointsAsString = nodeList.item(0).getAttributes().getNamedItem("points").getNodeValue(); + String[] points = pointsAsString.split(" "); + String[] coordinates = points[1].split(","); - elementView.setX((int)(x * Constants.DPI_RATIO)); - elementView.setY((int)(y * Constants.DPI_RATIO)); + double x = Double.parseDouble(coordinates[0]) + transformX; + double y = Double.parseDouble(coordinates[1]) + transformY; - minimumX = Math.min(elementView.getX(), minimumX); - minimumY = Math.min(elementView.getY(), minimumY); + elementView.setX((int) (x * Constants.DPI_RATIO)); + elementView.setY((int) (y * Constants.DPI_RATIO)); - ElementStyle style = view.getViewSet().getConfiguration().getStyles().findElementStyle(view.getModel().getElement(elementView.getId())); + minimumX = Math.min(elementView.getX(), minimumX); + minimumY = Math.min(elementView.getY(), minimumY); - maximumX = Math.max(elementView.getX() + style.getWidth(), maximumX); - maximumY = Math.max(elementView.getY() + style.getHeight(), maximumY); - } + ElementStyle style = view.getViewSet().getConfiguration().getStyles().findElementStyle(view.getModel().getElement(elementView.getId())); - for (RelationshipView relationshipView : view.getRelationships()) { - String expression = String.format("/svg/g/g[@id=\"%s\"]/path", relationshipView.getId()); - nodeList = (NodeList)xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); - if (nodeList.getLength() == 0) { - continue; + maximumX = Math.max(elementView.getX() + style.getWidth(), maximumX); + maximumY = Math.max(elementView.getY() + style.getHeight(), maximumY); } - String dAsString = nodeList.item(0).getAttributes().getNamedItem("d").getNodeValue(); - String[] d = dAsString.split(" "); - - Set vertices = new LinkedHashSet<>(); - - if (d.length == 3) { - relationshipView.setVertices(vertices); - } else { - for (int i = 1; i < d.length - 2; i++) { - double x = Double.parseDouble(d[i].split(",")[0]) + transformX; - double y = Double.parseDouble(d[i].split(",")[1]) + transformY; - Vertex vertex = new Vertex((int)(x * Constants.DPI_RATIO), (int)(y * Constants.DPI_RATIO)); - vertices.add(vertex); + for (RelationshipView relationshipView : view.getRelationships()) { + String expression = String.format("/svg/g/g[@id=\"%s\"]/path", relationshipView.getId()); + nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); + if (nodeList.getLength() == 0) { + continue; + } - minimumX = Math.min(vertex.getX(), minimumX); - minimumY = Math.min(vertex.getY(), minimumY); - maximumX = Math.max(vertex.getX(), maximumX); - maximumY = Math.max(vertex.getY(), maximumY); + String dAsString = nodeList.item(0).getAttributes().getNamedItem("d").getNodeValue(); + String[] d = dAsString.split(" "); + + Set vertices = new LinkedHashSet<>(); + + if (d.length == 3) { + relationshipView.setVertices(vertices); + } else { + for (int i = 1; i < d.length - 2; i++) { + double x = Double.parseDouble(d[i].split(",")[0]) + transformX; + double y = Double.parseDouble(d[i].split(",")[1]) + transformY; + Vertex vertex = new Vertex((int) (x * Constants.DPI_RATIO), (int) (y * Constants.DPI_RATIO)); + vertices.add(vertex); + + minimumX = Math.min(vertex.getX(), minimumX); + minimumY = Math.min(vertex.getY(), minimumY); + maximumX = Math.max(vertex.getX(), maximumX); + maximumY = Math.max(vertex.getY(), maximumY); + } + relationshipView.setVertices(vertices); } - relationshipView.setVertices(vertices); } - } - // also take into account any clusters that might be rendered outside the nodes - String expression = "/svg/g/g[@class=\"cluster\"]/polygon"; - nodeList = (NodeList)xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); - for (int i = 0; i < nodeList.getLength(); i++) { - String[] points = nodeList.item(i).getAttributes().getNamedItem("points").getNodeValue().split(" "); - for (String point : points) { - int x = (int)((Double.parseDouble(point.split(",")[0]) + transformX) * Constants.DPI_RATIO); - int y = (int)((Double.parseDouble(point.split(",")[1]) + transformY) * Constants.DPI_RATIO); - - minimumX = Math.min(x, minimumX); - minimumY = Math.min(y, minimumY); - maximumX = Math.max(x, maximumX); - maximumY = Math.max(y, maximumY); + // also take into account any clusters that might be rendered outside the nodes + String expression = "/svg/g/g[@class=\"cluster\"]/polygon"; + nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); + for (int i = 0; i < nodeList.getLength(); i++) { + String[] points = nodeList.item(i).getAttributes().getNamedItem("points").getNodeValue().split(" "); + for (String point : points) { + int x = (int) ((Double.parseDouble(point.split(",")[0]) + transformX) * Constants.DPI_RATIO); + int y = (int) ((Double.parseDouble(point.split(",")[1]) + transformY) * Constants.DPI_RATIO); + + minimumX = Math.min(x, minimumX); + minimumY = Math.min(y, minimumY); + maximumX = Math.max(x, maximumX); + maximumY = Math.max(y, maximumY); + } } - } - int pageWidth = Math.max(margin, maximumX + margin); - int pageHeight= Math.max(margin, maximumY + margin); + int pageWidth = Math.max(margin, maximumX + margin); + int pageHeight = Math.max(margin, maximumY + margin); - if (changePaperSize) { - view.setPaperSize(null); - view.setDimensions(new Dimensions(pageWidth, pageHeight)); + if (changePaperSize) { + view.setPaperSize(null); + view.setDimensions(new Dimensions(pageWidth, pageHeight)); - PaperSize.Orientation orientation = (pageWidth > pageHeight) ? PaperSize.Orientation.Landscape : PaperSize.Orientation.Portrait; - for (PaperSize paperSize : PaperSize.getOrderedPaperSizes(orientation)) { - if (paperSize.getWidth() > (pageWidth) && paperSize.getHeight() > (pageHeight)) { - view.setPaperSize(paperSize); - break; + PaperSize.Orientation orientation = (pageWidth > pageHeight) ? PaperSize.Orientation.Landscape : PaperSize.Orientation.Portrait; + for (PaperSize paperSize : PaperSize.getOrderedPaperSizes(orientation)) { + if (paperSize.getWidth() > (pageWidth) && paperSize.getHeight() > (pageHeight)) { + view.setPaperSize(paperSize); + break; + } } } - } - int deltaX = (pageWidth - maximumX + minimumX) / 2; - int deltaY = (pageHeight - maximumY + minimumY) / 2; + int deltaX = (pageWidth - maximumX + minimumX) / 2; + int deltaY = (pageHeight - maximumY + minimumY) / 2; - // move everything relative to 0,0 - for (ElementView elementView : view.getElements()) { - elementView.setX(elementView.getX() - minimumX); - elementView.setY(elementView.getY() - minimumY); - } - for (RelationshipView relationshipView : view.getRelationships()) { - for (Vertex vertex : relationshipView.getVertices()) { - vertex.setX(vertex.getX() - minimumX); - vertex.setY(vertex.getY() - minimumY); + // move everything relative to 0,0 + for (ElementView elementView : view.getElements()) { + elementView.setX(elementView.getX() - minimumX); + elementView.setY(elementView.getY() - minimumY); + } + for (RelationshipView relationshipView : view.getRelationships()) { + for (Vertex vertex : relationshipView.getVertices()) { + vertex.setX(vertex.getX() - minimumX); + vertex.setY(vertex.getY() - minimumY); + } } - } - // and now centre everything - for (ElementView elementView : view.getElements()) { - elementView.setX(elementView.getX() + deltaX); - elementView.setY(elementView.getY() + deltaY); - } - for (RelationshipView relationshipView : view.getRelationships()) { - for (Vertex vertex : relationshipView.getVertices()) { - vertex.setX(vertex.getX() + deltaX); - vertex.setY(vertex.getY() + deltaY); + // and now centre everything + for (ElementView elementView : view.getElements()) { + elementView.setX(elementView.getX() + deltaX); + elementView.setY(elementView.getY() + deltaY); } + for (RelationshipView relationshipView : view.getRelationships()) { + for (Vertex vertex : relationshipView.getVertices()) { + vertex.setX(vertex.getX() + deltaX); + vertex.setY(vertex.getY() + deltaY); + } + } + + log.debug("Layout applied to view with key " + view.getKey()); + } else { + log.error(file.getAbsolutePath() + " does not exist; layout not applied to view with key " + view.getKey()); } }