Skip to content
This repository has been archived by the owner on Jan 10, 2024. It is now read-only.

Commit

Permalink
Adds support for C4PlantUML sequence diagrams (closes #66) ... also r…
Browse files Browse the repository at this point in the history
…elationships are now rendered as `Rel` rather than `Rel_D`.
  • Loading branch information
simonbrowndotje committed Oct 27, 2023
1 parent 5b1e65a commit 9641da4
Show file tree
Hide file tree
Showing 17 changed files with 201 additions and 143 deletions.
6 changes: 4 additions & 2 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

## 1.17.0 (unreleased)

- C4PlantUMLExporter: Adds support for border style and stroke width (see https://github.com/structurizr/export/issues/73).
- MermaidDiagramExporter: Fixes https://github.com/structurizr/export/issues/80
- C4PlantUMLExporter: Adds support for border style and stroke width (#73).
- C4PlantUMLExporter: Adds support for sequence diagrams (#66).
- C4PlantUMLExporter: Relationships are now rendered as `Rel` rather than `Rel_D`.
- MermaidDiagramExporter: Fixes #80 (Mermaid render error when description contains `(` character).

## 1.16.1 (11th August 2023)

Expand Down
161 changes: 106 additions & 55 deletions src/main/java/com/structurizr/export/plantuml/C4PlantUMLExporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
import com.structurizr.util.StringUtils;
import com.structurizr.view.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;

import static java.lang.String.format;

Expand All @@ -23,6 +21,7 @@ public class C4PlantUMLExporter extends AbstractPlantUMLExporter {
public static final String C4PLANTUML_STANDARD_LIBRARY_PROPERTY = "c4plantuml.stdlib";
public static final String C4PLANTUML_SPRITE = "c4plantuml.sprite";
public static final String C4PLANTUML_SHADOW = "c4plantuml.shadow";
public static final String C4PLANTUML_SEQUENCE_DIAGRAM_PROPERTY = "plantuml.sequenceDiagram";

/**
* <p>Set this property to <code>true</code> by calling {@link Configuration#addProperty(String, String)} in your
Expand Down Expand Up @@ -68,50 +67,58 @@ protected void writeHeader(ModelView view, IndentingWriter writer) {

writeSkinParams(writer);

if (view.getAutomaticLayout() != null) {
switch (view.getAutomaticLayout().getRankDirection()) {
case LeftRight:
writer.writeLine("left to right direction");
break;
default:
writer.writeLine("top to bottom direction");
break;
if (renderAsSequenceDiagram(view)) {
if (usePlantUMLStandardLibrary(view)) {
writer.writeLine("!include <C4/C4_Sequence>");
} else {
writer.writeLine("!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Sequence.puml");
}
} else {
writer.writeLine("top to bottom direction");
}

writer.writeLine();
if (view.getAutomaticLayout() != null) {
switch (view.getAutomaticLayout().getRankDirection()) {
case LeftRight:
writer.writeLine("left to right direction");
break;
default:
writer.writeLine("top to bottom direction");
break;
}
} else {
writer.writeLine("top to bottom direction");
}

if (usePlantUMLStandardLibrary(view)) {
writer.writeLine("!include <C4/C4>");
writer.writeLine("!include <C4/C4_Context>");
} else {
writer.writeLine("!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4.puml");
writer.writeLine("!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml");
}
writer.writeLine();

if (view.getElements().stream().map(ElementView::getElement).anyMatch(e -> e instanceof Container || e instanceof ContainerInstance)) {
if (usePlantUMLStandardLibrary(view)) {
writer.writeLine("!include <C4/C4_Container>");
writer.writeLine("!include <C4/C4>");
writer.writeLine("!include <C4/C4_Context>");
} else {
writer.writeLine("!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml");
writer.writeLine("!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4.puml");
writer.writeLine("!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml");
}
}

if (view.getElements().stream().map(ElementView::getElement).anyMatch(e -> e instanceof Component)) {
if (usePlantUMLStandardLibrary(view)) {
writer.writeLine("!include <C4/C4_Component>");
} else {
writer.writeLine("!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml");
if (view.getElements().stream().map(ElementView::getElement).anyMatch(e -> e instanceof Container || e instanceof ContainerInstance)) {
if (usePlantUMLStandardLibrary(view)) {
writer.writeLine("!include <C4/C4_Container>");
} else {
writer.writeLine("!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml");
}
}
}

if (view instanceof DeploymentView) {
if (usePlantUMLStandardLibrary(view)) {
writer.writeLine("!include <C4/C4_Deployment>");
} else {
writer.writeLine("!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Deployment.puml");
if (view.getElements().stream().map(ElementView::getElement).anyMatch(e -> e instanceof Component)) {
if (usePlantUMLStandardLibrary(view)) {
writer.writeLine("!include <C4/C4_Component>");
} else {
writer.writeLine("!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml");
}
}

if (view instanceof DeploymentView) {
if (usePlantUMLStandardLibrary(view)) {
writer.writeLine("!include <C4/C4_Deployment>");
} else {
writer.writeLine("!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Deployment.puml");
}
}
}

Expand All @@ -138,24 +145,28 @@ protected void writeHeader(ModelView view, IndentingWriter writer) {
relationshipStyles.put(relationshipStyle.getTag(), relationshipStyle);
}

// boundaries
List<Element> boundaryElements = new ArrayList<>();
if (view instanceof ContainerView) {
boundaryElements.addAll(getBoundarySoftwareSystems(view));
} else if (view instanceof ComponentView) {
boundaryElements.addAll(getBoundaryContainers(view));
} else if (view instanceof DynamicView) {
DynamicView dynamicView = (DynamicView)view;
if (dynamicView.getElement() instanceof SoftwareSystem) {
if (renderAsSequenceDiagram(view)) {
// no boundaries, do nothing
} else {
// boundaries
List<Element> boundaryElements = new ArrayList<>();
if (view instanceof ContainerView) {
boundaryElements.addAll(getBoundarySoftwareSystems(view));
} else if (dynamicView.getElement() instanceof Container) {
} else if (view instanceof ComponentView) {
boundaryElements.addAll(getBoundaryContainers(view));
} else if (view instanceof DynamicView) {
DynamicView dynamicView = (DynamicView) view;
if (dynamicView.getElement() instanceof SoftwareSystem) {
boundaryElements.addAll(getBoundarySoftwareSystems(view));
} else if (dynamicView.getElement() instanceof Container) {
boundaryElements.addAll(getBoundaryContainers(view));
}
}
}

for (Element boundaryElement : boundaryElements) {
ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(boundaryElement);
boundaryStyles.put(elementStyle.getTag(), elementStyle);
for (Element boundaryElement : boundaryElements) {
ElementStyle elementStyle = view.getViewSet().getConfiguration().getStyles().findElementStyle(boundaryElement);
boundaryStyles.put(elementStyle.getTag(), elementStyle);
}
}

if (!elementStyles.isEmpty()) {
Expand Down Expand Up @@ -406,6 +417,38 @@ public Diagram export(CustomView view) {
return null;
}

@Override
public Diagram export(DynamicView view, String order) {
if (renderAsSequenceDiagram(view)) {
IndentingWriter writer = new IndentingWriter();
writeHeader(view, writer);

boolean elementsWritten = false;

Set<Element> elements = new LinkedHashSet<>();
for (RelationshipView relationshipView : view.getRelationships()) {
elements.add(relationshipView.getRelationship().getSource());
elements.add(relationshipView.getRelationship().getDestination());
}

for (Element element : elements) {
writeElement(view, element, writer);
elementsWritten = true;
}

if (elementsWritten) {
writer.writeLine();
}

writeRelationships(view, writer);
writeFooter(view, writer);

return createDiagram(view, writer.toString());
} else {
return super.export(view, order);
}
}

@Override
protected void writeElement(ModelView view, Element element, IndentingWriter writer) {
if (element instanceof CustomElement) {
Expand Down Expand Up @@ -565,8 +608,12 @@ protected void writeRelationship(ModelView view, RelationshipView relationshipVi

String description = "";

if (!StringUtils.isNullOrEmpty(relationshipView.getOrder())) {
description = relationshipView.getOrder() + ". ";
if (renderAsSequenceDiagram(view)) {
// do nothing - sequence diagrams don't need the order
} else {
if (!StringUtils.isNullOrEmpty(relationshipView.getOrder())) {
description = relationshipView.getOrder() + ". ";
}
}

description += (hasValue(relationshipView.getDescription()) ? relationshipView.getDescription() : hasValue(relationshipView.getRelationship().getDescription()) ? relationshipView.getRelationship().getDescription() : "");
Expand All @@ -583,7 +630,7 @@ protected void writeRelationship(ModelView view, RelationshipView relationshipVi

// Rel(from, to, label, ?techn, ?descr, ?sprite, ?tags, ?link)
writer.writeLine(
format("Rel_D(%s, %s, \"%s\", $techn=\"%s\", $tags=\"%s\", $link=\"%s\")",
format("Rel(%s, %s, \"%s\", $techn=\"%s\", $tags=\"%s\", $link=\"%s\")",
idOf(source), idOf(destination), description, technology, tagsOf(view, relationship), url)
);
}
Expand Down Expand Up @@ -626,4 +673,8 @@ protected boolean usePlantUMLStandardLibrary(ModelView view) {
return "true".equalsIgnoreCase(getViewOrViewSetProperty(view, C4PLANTUML_STANDARD_LIBRARY_PROPERTY, "true"));
}

protected boolean renderAsSequenceDiagram(ModelView view) {
return view instanceof DynamicView && "true".equalsIgnoreCase(getViewOrViewSetProperty(view, C4PLANTUML_SEQUENCE_DIAGRAM_PROPERTY, "false"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ public void test_BigBankPlcExample() throws Exception {
diagram = diagrams.stream().filter(md -> md.getKey().equals("LiveDeployment")).findFirst().get();
expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-LiveDeployment.puml"));
assertEquals(expected, diagram.getDefinition());

// and the sequence diagram version
workspace.getViews().getConfiguration().addProperty(exporter.C4PLANTUML_SEQUENCE_DIAGRAM_PROPERTY, "true");
diagrams = exporter.export(workspace);
diagram = diagrams.stream().filter(d -> d.getKey().equals("SignIn")).findFirst().get();
expected = readFile(new File("./src/test/java/com/structurizr/export/plantuml/c4plantuml/36141-SignIn-sequence.puml"));
assertEquals(expected, diagram.getDefinition());
}

@Test
Expand Down Expand Up @@ -222,7 +229,7 @@ public void test_renderContainerDiagramWithExternalContainers() {
" Container(SoftwareSystem2.Container2, \"Container 2\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" +
"}\n" +
"\n" +
"Rel_D(SoftwareSystem1.Container1, SoftwareSystem2.Container2, \"Uses\", $techn=\"\", $tags=\"\", $link=\"\")\n" +
"Rel(SoftwareSystem1.Container1, SoftwareSystem2.Container2, \"Uses\", $techn=\"\", $tags=\"\", $link=\"\")\n" +
"\n" +
"SHOW_LEGEND(true)\n" +
"@enduml", diagram.getDefinition());
Expand Down Expand Up @@ -264,7 +271,7 @@ public void test_renderComponentDiagramWithExternalComponents() {
" Component(SoftwareSystem2.Container2.Component2, \"Component 2\", $techn=\"\", $descr=\"\", $tags=\"\", $link=\"\")\n" +
"}\n" +
"\n" +
"Rel_D(SoftwareSystem1.Container1.Component1, SoftwareSystem2.Container2.Component2, \"Uses\", $techn=\"\", $tags=\"\", $link=\"\")\n" +
"Rel(SoftwareSystem1.Container1.Component1, SoftwareSystem2.Container2.Component2, \"Uses\", $techn=\"\", $tags=\"\", $link=\"\")\n" +
"\n" +
"SHOW_LEGEND(true)\n" +
"@enduml", diagram.getDefinition());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,19 @@ Container_Boundary("InternetBankingSystem.APIApplication_boundary", "API Applica
Component(InternetBankingSystem.APIApplication.EmailComponent, "E-mail Component", $techn="Spring Bean", $descr="Sends e-mails to users.", $tags="Component", $link="")
}

Rel_D(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="")
Rel_D(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.ResetPasswordController, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="")
Rel_D(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.AccountsSummaryController, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="")
Rel_D(InternetBankingSystem.MobileApp, InternetBankingSystem.APIApplication.SignInController, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="")
Rel_D(InternetBankingSystem.MobileApp, InternetBankingSystem.APIApplication.ResetPasswordController, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="")
Rel_D(InternetBankingSystem.MobileApp, InternetBankingSystem.APIApplication.AccountsSummaryController, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="")
Rel_D(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "Uses", $techn="", $tags="Relationship", $link="")
Rel_D(InternetBankingSystem.APIApplication.AccountsSummaryController, InternetBankingSystem.APIApplication.MainframeBankingSystemFacade, "Uses", $techn="", $tags="Relationship", $link="")
Rel_D(InternetBankingSystem.APIApplication.ResetPasswordController, InternetBankingSystem.APIApplication.SecurityComponent, "Uses", $techn="", $tags="Relationship", $link="")
Rel_D(InternetBankingSystem.APIApplication.ResetPasswordController, InternetBankingSystem.APIApplication.EmailComponent, "Uses", $techn="", $tags="Relationship", $link="")
Rel_D(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "Reads from and writes to", $techn="JDBC", $tags="Relationship", $link="")
Rel_D(InternetBankingSystem.APIApplication.MainframeBankingSystemFacade, MainframeBankingSystem, "Uses", $techn="XML/HTTPS", $tags="Relationship", $link="")
Rel_D(InternetBankingSystem.APIApplication.EmailComponent, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="")
Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.SignInController, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="")
Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.ResetPasswordController, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="")
Rel(InternetBankingSystem.SinglePageApplication, InternetBankingSystem.APIApplication.AccountsSummaryController, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="")
Rel(InternetBankingSystem.MobileApp, InternetBankingSystem.APIApplication.SignInController, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="")
Rel(InternetBankingSystem.MobileApp, InternetBankingSystem.APIApplication.ResetPasswordController, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="")
Rel(InternetBankingSystem.MobileApp, InternetBankingSystem.APIApplication.AccountsSummaryController, "Makes API calls to", $techn="JSON/HTTPS", $tags="Relationship", $link="")
Rel(InternetBankingSystem.APIApplication.SignInController, InternetBankingSystem.APIApplication.SecurityComponent, "Uses", $techn="", $tags="Relationship", $link="")
Rel(InternetBankingSystem.APIApplication.AccountsSummaryController, InternetBankingSystem.APIApplication.MainframeBankingSystemFacade, "Uses", $techn="", $tags="Relationship", $link="")
Rel(InternetBankingSystem.APIApplication.ResetPasswordController, InternetBankingSystem.APIApplication.SecurityComponent, "Uses", $techn="", $tags="Relationship", $link="")
Rel(InternetBankingSystem.APIApplication.ResetPasswordController, InternetBankingSystem.APIApplication.EmailComponent, "Uses", $techn="", $tags="Relationship", $link="")
Rel(InternetBankingSystem.APIApplication.SecurityComponent, InternetBankingSystem.Database, "Reads from and writes to", $techn="JDBC", $tags="Relationship", $link="")
Rel(InternetBankingSystem.APIApplication.MainframeBankingSystemFacade, MainframeBankingSystem, "Uses", $techn="XML/HTTPS", $tags="Relationship", $link="")
Rel(InternetBankingSystem.APIApplication.EmailComponent, EmailSystem, "Sends e-mail using", $techn="", $tags="Relationship", $link="")

SHOW_LEGEND(true)
@enduml
Loading

0 comments on commit 9641da4

Please sign in to comment.